Skip to main content

lib/quickbeam/napi.zig

const types = @import("types.zig");
const js = @import("js_helpers.zig");
const std = types.std;
const qjs = types.qjs;
const gpa = types.gpa;

const nt = @import("napi_types.zig");
const common = @import("napi/common.zig");
const buffers = @import("napi/buffers.zig");
const wrap_mod = @import("napi/wrap.zig");
const async_work = @import("napi/async_work.zig");
const tsfn = @import("napi/tsfn.zig");
pub const Status = nt.Status;
pub const napi_status = nt.napi_status;
pub const napi_env = nt.napi_env;
pub const NapiEnv = nt.NapiEnv;
pub const napi_value = nt.napi_value;
pub const napi_ref = nt.napi_ref;
pub const napi_callback = nt.napi_callback;
pub const napi_callback_info = nt.napi_callback_info;
pub const napi_finalize = nt.napi_finalize;
pub const napi_handle_scope = nt.napi_handle_scope;
pub const napi_escapable_handle_scope = nt.napi_escapable_handle_scope;
pub const napi_deferred = nt.napi_deferred;
pub const napi_property_descriptor = nt.napi_property_descriptor;
pub const napi_valuetype = nt.napi_valuetype;
pub const napi_typedarray_type = nt.napi_typedarray_type;
pub const HandleScope = nt.HandleScope;
pub const NapiReference = nt.NapiReference;
pub const Deferred = nt.Deferred;
pub const CallbackInfo = nt.CallbackInfo;
pub const FunctionCallbackData = nt.FunctionCallbackData;
pub const ExternalData = nt.ExternalData;
pub const AsyncWork = nt.AsyncWork;
pub const napi_async_work = nt.napi_async_work;
pub const ThreadSafeFunction = nt.ThreadSafeFunction;
pub const napi_threadsafe_function = nt.napi_threadsafe_function;
pub const NAPI_AUTO_LENGTH = nt.NAPI_AUTO_LENGTH;

fn toVal(v: napi_value) qjs.JSValue {
    return common.toVal(v);
}

// ──────────────────── Globals / Version ────────────────────

pub export fn napi_get_undefined(env_: napi_env, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = env.createNapiValue(js.js_undefined());
    return env.ok();
}

pub export fn napi_get_null(env_: napi_env, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = env.createNapiValue(js.js_null());
    return env.ok();
}

pub export fn napi_get_global(env_: napi_env, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const global = qjs.JS_GetGlobalObject(env.ctx);
    r.* = env.createNapiValue(global);
    return env.ok();
}

pub export fn napi_get_boolean(env_: napi_env, value: bool, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = env.createNapiValue(if (value) js.js_true() else js.js_false());
    return env.ok();
}

// ──────────────────── Value Creation ────────────────────

pub export fn napi_create_int32(env_: napi_env, value: i32, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = env.createNapiValue(qjs.JS_NewInt32(env.ctx, value));
    return env.ok();
}

pub export fn napi_create_uint32(env_: napi_env, value: u32, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = env.createNapiValue(qjs.JS_NewUint32(env.ctx, value));
    return env.ok();
}

pub export fn napi_create_int64(env_: napi_env, value: i64, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = env.createNapiValue(qjs.JS_NewInt64(env.ctx, value));
    return env.ok();
}

pub export fn napi_create_double(env_: napi_env, value: f64, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = env.createNapiValue(qjs.JS_NewFloat64(env.ctx, value));
    return env.ok();
}

pub export fn napi_create_string_utf8(env_: napi_env, str: ?[*]const u8, length: usize, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const slice = napiSpan(str, length) orelse return env.invalidArg();
    const val = qjs.JS_NewStringLen(env.ctx, slice.ptr, slice.len);
    r.* = env.createNapiValue(val);
    return env.ok();
}

pub export fn napi_create_string_latin1(env_: napi_env, str: ?[*]const u8, length: usize, result: ?*napi_value) callconv(.c) napi_status {
    // QuickJS strings are UTF-8; latin1 is a subset for 0-127 and close enough for basic use
    return napi_create_string_utf8(env_, str, length, result);
}

pub export fn napi_create_string_utf16(env_: napi_env, str: ?[*]const u16, length: usize, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();

    const slice: []const u16 = blk: {
        if (str) |ptr| {
            if (length == NAPI_AUTO_LENGTH) {
                var len: usize = 0;
                while (ptr[len] != 0) : (len += 1) {}
                break :blk ptr[0..len];
            }
            break :blk ptr[0..length];
        }
        return env.invalidArg();
    };

    // Convert UTF-16 to UTF-8
    var buf: std.ArrayListUnmanaged(u8) = .{};
    defer buf.deinit(gpa);
    var i: usize = 0;
    while (i < slice.len) {
        var codepoint: u21 = slice[i];
        i += 1;
        // Handle surrogate pairs
        if (codepoint >= 0xD800 and codepoint <= 0xDBFF and i < slice.len) {
            const low = slice[i];
            if (low >= 0xDC00 and low <= 0xDFFF) {
                codepoint = 0x10000 + ((@as(u21, @intCast(codepoint)) - 0xD800) << 10) + (@as(u21, @intCast(low)) - 0xDC00);
                i += 1;
            }
        }
        var utf8_buf: [4]u8 = undefined;
        const utf8_len = std.unicode.utf8Encode(codepoint, &utf8_buf) catch continue;
        buf.appendSlice(gpa, utf8_buf[0..utf8_len]) catch return env.genericFailure();
    }

    const val = qjs.JS_NewStringLen(env.ctx, buf.items.ptr, buf.items.len);
    r.* = env.createNapiValue(val);
    return env.ok();
}

pub export fn napi_create_symbol(env_: napi_env, description: napi_value, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();

    const desc_val = toVal(description);
    var sym: qjs.JSValue = js.js_undefined();
    if (qjs.JS_IsString(desc_val)) {
        const cstr = qjs.JS_ToCString(env.ctx, desc_val);
        if (cstr != null) {
            sym = qjs.JS_NewSymbol(env.ctx, cstr, false);
            qjs.JS_FreeCString(env.ctx, cstr);
        } else {
            sym = qjs.JS_NewSymbol(env.ctx, null, false);
        }
    } else {
        sym = qjs.JS_NewSymbol(env.ctx, null, false);
    }

    r.* = env.createNapiValue(sym);
    return env.ok();
}

pub export fn napi_create_object(env_: napi_env, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const obj = qjs.JS_NewObject(env.ctx);
    r.* = env.createNapiValue(obj);
    return env.ok();
}

pub export fn napi_create_array(env_: napi_env, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const arr = qjs.JS_NewArray(env.ctx);
    r.* = env.createNapiValue(arr);
    return env.ok();
}

pub export fn napi_create_array_with_length(env_: napi_env, length: usize, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const arr = qjs.JS_NewArray(env.ctx);
    if (length > 0) {
        _ = qjs.JS_SetPropertyStr(env.ctx, arr, "length", qjs.JS_NewUint32(env.ctx, @intCast(length)));
    }
    r.* = env.createNapiValue(arr);
    return env.ok();
}

// ──────────────────── Value Getters ────────────────────

pub export fn napi_get_value_double(env_: napi_env, value: napi_value, result: ?*f64) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);
    var d: f64 = 0;
    if (qjs.JS_ToFloat64(env.ctx, &d, val) < 0) return env.setLastError(.number_expected);
    r.* = d;
    return env.ok();
}

pub export fn napi_get_value_int32(env_: napi_env, value: napi_value, result: ?*i32) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);
    var i: i32 = 0;
    if (qjs.JS_ToInt32(env.ctx, &i, val) < 0) return env.setLastError(.number_expected);
    r.* = i;
    return env.ok();
}

pub export fn napi_get_value_uint32(env_: napi_env, value: napi_value, result: ?*u32) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);
    var u: u32 = 0;
    if (qjs.JS_ToUint32(env.ctx, &u, val) < 0) return env.setLastError(.number_expected);
    r.* = u;
    return env.ok();
}

pub export fn napi_get_value_int64(env_: napi_env, value: napi_value, result: ?*i64) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);
    var i: i64 = 0;
    if (qjs.JS_ToInt64(env.ctx, &i, val) < 0) return env.setLastError(.number_expected);
    r.* = i;
    return env.ok();
}

pub export fn napi_get_value_bool(env_: napi_env, value: napi_value, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);
    if (!qjs.JS_IsBool(val)) return env.setLastError(.boolean_expected);
    r.* = qjs.JS_ToBool(env.ctx, val) != 0;
    return env.ok();
}

pub export fn napi_get_value_string_utf8(env_: napi_env, value: napi_value, buf: ?[*]u8, bufsize: usize, result_ptr: ?*usize) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const val = toVal(value);
    if (!qjs.JS_IsString(val)) return env.setLastError(.string_expected);

    var len: usize = 0;
    const cstr = qjs.JS_ToCStringLen(env.ctx, &len, val);
    if (cstr == null) return env.genericFailure();
    defer qjs.JS_FreeCString(env.ctx, cstr);

    if (buf) |b| {
        if (bufsize > 0) {
            const copy_len = @min(len, bufsize - 1);
            @memcpy(b[0..copy_len], cstr[0..copy_len]);
            b[copy_len] = 0;
            if (result_ptr) |rp| rp.* = copy_len;
        }
    } else {
        if (result_ptr) |rp| rp.* = len;
    }
    return env.ok();
}

pub export fn napi_get_value_string_latin1(env_: napi_env, value: napi_value, buf: ?[*]u8, bufsize: usize, result_ptr: ?*usize) callconv(.c) napi_status {
    return napi_get_value_string_utf8(env_, value, buf, bufsize, result_ptr);
}

pub export fn napi_get_value_string_utf16(env_: napi_env, value: napi_value, buf: ?[*]u16, bufsize: usize, result_ptr: ?*usize) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const val = toVal(value);
    if (!qjs.JS_IsString(val)) return env.setLastError(.string_expected);

    var len: usize = 0;
    const cstr = qjs.JS_ToCStringLen(env.ctx, &len, val);
    if (cstr == null) return env.genericFailure();
    defer qjs.JS_FreeCString(env.ctx, cstr);

    const utf8_slice = cstr[0..len];

    if (buf) |b| {
        if (bufsize == 0) {
            if (result_ptr) |rp| rp.* = len; // approximate
            return env.ok();
        }
        var out_idx: usize = 0;
        var view = std.unicode.Utf8View.initUnchecked(utf8_slice);
        var it = view.iterator();
        while (it.nextCodepoint()) |cp| {
            if (cp >= 0x10000) {
                if (out_idx + 2 >= bufsize) break;
                const adjusted = cp - 0x10000;
                b[out_idx] = @intCast(0xD800 + (adjusted >> 10));
                b[out_idx + 1] = @intCast(0xDC00 + (adjusted & 0x3FF));
                out_idx += 2;
            } else {
                if (out_idx + 1 >= bufsize) break;
                b[out_idx] = @intCast(cp);
                out_idx += 1;
            }
        }
        b[out_idx] = 0;
        if (result_ptr) |rp| rp.* = out_idx;
    } else {
        // Count UTF-16 code units
        var count: usize = 0;
        var view = std.unicode.Utf8View.initUnchecked(utf8_slice);
        var it = view.iterator();
        while (it.nextCodepoint()) |cp| {
            count += if (cp >= 0x10000) 2 else 1;
        }
        if (result_ptr) |rp| rp.* = count;
    }
    return env.ok();
}

// ──────────────────── Type Checks ────────────────────

pub export fn napi_typeof(env_: napi_env, value: napi_value, result: ?*napi_valuetype) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);

    const tag = qjs.JS_VALUE_GET_TAG(val);
    r.* = switch (tag) {
        qjs.JS_TAG_UNDEFINED => .undefined,
        qjs.JS_TAG_NULL => .null,
        qjs.JS_TAG_BOOL => .boolean,
        qjs.JS_TAG_INT, qjs.JS_TAG_FLOAT64 => .number,
        qjs.JS_TAG_STRING => .string,
        qjs.JS_TAG_SYMBOL => .symbol,
        qjs.JS_TAG_BIG_INT, qjs.JS_TAG_SHORT_BIG_INT => .bigint,
        qjs.JS_TAG_OBJECT => blk: {
            if (qjs.JS_IsFunction(env.ctx, val)) break :blk .function;
            if (nt.external_class_id != 0 and qjs.JS_GetClassID(val) == nt.external_class_id) break :blk .external;
            break :blk .object;
        },
        else => .undefined,
    };
    return env.ok();
}

pub export fn napi_is_array(env_: napi_env, value: napi_value, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = qjs.JS_IsArray(toVal(value));
    return env.ok();
}

pub export fn napi_is_arraybuffer(env_: napi_env, value: napi_value, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = qjs.JS_IsArrayBuffer(toVal(value));
    return env.ok();
}

pub export fn napi_is_date(env_: napi_env, value: napi_value, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);
    if (!qjs.JS_IsObject(val)) {
        r.* = false;
        return env.ok();
    }
    // Check if it has getTime method (duck typing for Date)
    const get_time = qjs.JS_GetPropertyStr(env.ctx, val, "getTime");
    r.* = qjs.JS_IsFunction(env.ctx, get_time);
    qjs.JS_FreeValue(env.ctx, get_time);
    return env.ok();
}

pub export fn napi_is_error(env_: napi_env, value: napi_value, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = qjs.JS_IsError(toVal(value));
    return env.ok();
}

pub export fn napi_is_promise(env_: napi_env, value: napi_value, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = js.is_promise(env.ctx, toVal(value));
    return env.ok();
}

pub export fn napi_is_dataview(env_: napi_env, value: napi_value, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);
    if (!qjs.JS_IsObject(val)) {
        r.* = false;
        return env.ok();
    }
    // Check constructor name
    const ctor = qjs.JS_GetPropertyStr(env.ctx, val, "constructor");
    defer qjs.JS_FreeValue(env.ctx, ctor);
    if (qjs.JS_IsObject(ctor)) {
        const name = qjs.JS_GetPropertyStr(env.ctx, ctor, "name");
        defer qjs.JS_FreeValue(env.ctx, name);
        if (qjs.JS_IsString(name)) {
            var slen: usize = 0;
            const cstr = qjs.JS_ToCStringLen(env.ctx, &slen, name);
            if (cstr != null) {
                defer qjs.JS_FreeCString(env.ctx, cstr);
                r.* = std.mem.eql(u8, cstr[0..slen], "DataView");
                return env.ok();
            }
        }
    }
    r.* = false;
    return env.ok();
}

pub export fn napi_strict_equals(env_: napi_env, lhs: napi_value, rhs: napi_value, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const a = toVal(lhs);
    const b = toVal(rhs);

    const tag_a = qjs.JS_VALUE_GET_TAG(a);
    const tag_b = qjs.JS_VALUE_GET_TAG(b);

    if (tag_a != tag_b) {
        // Special case: INT and FLOAT64 can be strictly equal
        if ((tag_a == qjs.JS_TAG_INT or tag_a == qjs.JS_TAG_FLOAT64) and
            (tag_b == qjs.JS_TAG_INT or tag_b == qjs.JS_TAG_FLOAT64))
        {
            var da: f64 = 0;
            var db: f64 = 0;
            _ = qjs.JS_ToFloat64(env.ctx, &da, a);
            _ = qjs.JS_ToFloat64(env.ctx, &db, b);
            r.* = da == db;
            return env.ok();
        }
        r.* = false;
        return env.ok();
    }

    r.* = switch (tag_a) {
        qjs.JS_TAG_UNDEFINED, qjs.JS_TAG_NULL => true,
        qjs.JS_TAG_BOOL => qjs.JS_VALUE_GET_INT(a) == qjs.JS_VALUE_GET_INT(b),
        qjs.JS_TAG_INT => qjs.JS_VALUE_GET_INT(a) == qjs.JS_VALUE_GET_INT(b),
        qjs.JS_TAG_FLOAT64 => blk: {
            var da: f64 = 0;
            var db: f64 = 0;
            _ = qjs.JS_ToFloat64(env.ctx, &da, a);
            _ = qjs.JS_ToFloat64(env.ctx, &db, b);
            break :blk da == db;
        },
        qjs.JS_TAG_STRING => qjs.JS_IsStrictEqual(env.ctx, a, b),
        else => qjs.JS_VALUE_GET_PTR(a) == qjs.JS_VALUE_GET_PTR(b),
    };
    return env.ok();
}

// ──────────────────── Coercion ────────────────────

pub export fn napi_coerce_to_bool(env_: napi_env, value: napi_value, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const b = qjs.JS_ToBool(env.ctx, toVal(value));
    r.* = env.createNapiValue(if (b != 0) js.js_true() else js.js_false());
    return env.ok();
}

pub export fn napi_coerce_to_number(env_: napi_env, value: napi_value, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    var d: f64 = 0;
    if (qjs.JS_ToFloat64(env.ctx, &d, toVal(value)) < 0) return env.setLastError(.pending_exception);
    const val = qjs.JS_NewFloat64(env.ctx, d);
    r.* = env.createNapiValue(val);
    return env.ok();
}

pub export fn napi_coerce_to_object(env_: napi_env, value: napi_value, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);
    const obj = qjs.JS_ToObject(env.ctx, val);
    if (js.js_is_exception(obj)) return env.setLastError(.pending_exception);
    r.* = env.createNapiValue(obj);
    return env.ok();
}

pub export fn napi_coerce_to_string(env_: napi_env, value: napi_value, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = qjs.JS_ToString(env.ctx, toVal(value));
    if (js.js_is_exception(val)) return env.setLastError(.pending_exception);
    r.* = env.createNapiValue(val);
    return env.ok();
}

// ──────────────────── Object Property Access ────────────────────

pub export fn napi_get_property(env_: napi_env, object: napi_value, key: napi_value, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);

    const k = toVal(key);
    const atom = qjs.JS_ValueToAtom(env.ctx, k);
    if (atom == 0) return env.invalidArg();
    defer qjs.JS_FreeAtom(env.ctx, atom);

    const val = qjs.JS_GetProperty(env.ctx, obj, atom);
    if (js.js_is_exception(val)) return env.setLastError(.pending_exception);
    r.* = env.createNapiValue(val);
    return env.ok();
}

pub export fn napi_set_property(env_: napi_env, object: napi_value, key: napi_value, value: napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);

    const k = toVal(key);
    const v = toVal(value);
    const atom = qjs.JS_ValueToAtom(env.ctx, k);
    if (atom == 0) return env.invalidArg();
    defer qjs.JS_FreeAtom(env.ctx, atom);

    const ret = qjs.JS_SetProperty(env.ctx, obj, atom, qjs.JS_DupValue(env.ctx, v));
    if (ret < 0) return env.setLastError(.pending_exception);
    return env.ok();
}

pub export fn napi_has_property(env_: napi_env, object: napi_value, key: napi_value, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);

    const k = toVal(key);
    const atom = qjs.JS_ValueToAtom(env.ctx, k);
    if (atom == 0) return env.invalidArg();
    defer qjs.JS_FreeAtom(env.ctx, atom);

    const ret = qjs.JS_HasProperty(env.ctx, obj, atom);
    if (ret < 0) return env.setLastError(.pending_exception);
    r.* = ret != 0;
    return env.ok();
}

pub export fn napi_delete_property(env_: napi_env, object: napi_value, key: napi_value, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);

    const k = toVal(key);
    const atom = qjs.JS_ValueToAtom(env.ctx, k);
    if (atom == 0) return env.invalidArg();
    defer qjs.JS_FreeAtom(env.ctx, atom);

    const ret = qjs.JS_DeleteProperty(env.ctx, obj, atom, 0);
    if (result) |r| r.* = ret >= 0;
    return env.ok();
}

pub export fn napi_get_named_property(env_: napi_env, object: napi_value, utf8name: [*c]const u8, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);
    if (utf8name == null) return env.invalidArg();

    const val = qjs.JS_GetPropertyStr(env.ctx, obj, utf8name);
    if (js.js_is_exception(val)) return env.setLastError(.pending_exception);
    r.* = env.createNapiValue(val);
    return env.ok();
}

pub export fn napi_set_named_property(env_: napi_env, object: napi_value, utf8name: [*c]const u8, value: napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);
    if (utf8name == null) return env.invalidArg();

    const v = toVal(value);
    const ret = qjs.JS_SetPropertyStr(env.ctx, obj, utf8name, qjs.JS_DupValue(env.ctx, v));
    if (ret < 0) return env.setLastError(.pending_exception);
    return env.ok();
}

pub export fn napi_has_named_property(env_: napi_env, object: napi_value, utf8name: [*c]const u8, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);
    if (utf8name == null) return env.invalidArg();

    const atom = qjs.JS_NewAtom(env.ctx, utf8name);
    defer qjs.JS_FreeAtom(env.ctx, atom);
    const ret = qjs.JS_HasProperty(env.ctx, obj, atom);
    r.* = ret > 0;
    return env.ok();
}

pub export fn napi_has_own_property(env_: napi_env, object: napi_value, key: napi_value, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);

    const k = toVal(key);
    const atom = qjs.JS_ValueToAtom(env.ctx, k);
    if (atom == 0) return env.invalidArg();
    defer qjs.JS_FreeAtom(env.ctx, atom);

    var desc = std.mem.zeroes(qjs.JSPropertyDescriptor);
    const ret = qjs.JS_GetOwnProperty(env.ctx, &desc, obj, atom);
    if (ret > 0) {
        qjs.JS_FreeValue(env.ctx, desc.value);
        if (desc.flags & qjs.JS_PROP_GETSET != 0) {
            qjs.JS_FreeValue(env.ctx, desc.getter);
            qjs.JS_FreeValue(env.ctx, desc.setter);
        }
    }
    r.* = ret > 0;
    return env.ok();
}

// ──────────────────── Array Element Access ────────────────────

pub export fn napi_set_element(env_: napi_env, object: napi_value, index: u32, value: napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);
    const v = toVal(value);
    const ret = qjs.JS_SetPropertyUint32(env.ctx, obj, index, qjs.JS_DupValue(env.ctx, v));
    if (ret < 0) return env.setLastError(.pending_exception);
    return env.ok();
}

pub export fn napi_get_element(env_: napi_env, object: napi_value, index: u32, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);
    const val = qjs.JS_GetPropertyUint32(env.ctx, obj, index);
    if (js.js_is_exception(val)) return env.setLastError(.pending_exception);
    r.* = env.createNapiValue(val);
    return env.ok();
}

pub export fn napi_has_element(env_: napi_env, object: napi_value, index: u32, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);
    const atom = qjs.JS_NewAtomUInt32(env.ctx, index);
    defer qjs.JS_FreeAtom(env.ctx, atom);
    const ret = qjs.JS_HasProperty(env.ctx, obj, atom);
    r.* = ret > 0;
    return env.ok();
}

pub export fn napi_delete_element(env_: napi_env, object: napi_value, index: u32, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);
    const atom = qjs.JS_NewAtomUInt32(env.ctx, index);
    defer qjs.JS_FreeAtom(env.ctx, atom);
    const ret = qjs.JS_DeleteProperty(env.ctx, obj, atom, 0);
    if (result) |r| r.* = ret >= 0;
    return env.ok();
}

pub export fn napi_get_array_length(env_: napi_env, value: napi_value, result: ?*u32) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);
    const len_val = qjs.JS_GetPropertyStr(env.ctx, val, "length");
    defer qjs.JS_FreeValue(env.ctx, len_val);
    var len: u32 = 0;
    _ = qjs.JS_ToUint32(env.ctx, &len, len_val);
    r.* = len;
    return env.ok();
}

// ──────────────────── Prototype ────────────────────

pub export fn napi_get_prototype(env_: napi_env, object: napi_value, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);
    const proto = qjs.JS_GetPrototype(env.ctx, obj);
    r.* = env.createNapiValue(proto);
    return env.ok();
}

// ──────────────────── Handle Scopes ────────────────────

pub export fn napi_open_handle_scope(env_: napi_env, result: ?*napi_handle_scope) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const scope = HandleScope.init(false);
    env.scope_stack.append(gpa, scope) catch return env.genericFailure();
    r.* = scope;
    return env.ok();
}

pub export fn napi_close_handle_scope(env_: napi_env, scope_: napi_handle_scope) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const scope = scope_ orelse return env.invalidArg();
    if (env.scope_stack.items.len > 0 and env.scope_stack.items[env.scope_stack.items.len - 1] == scope) {
        _ = env.scope_stack.pop();
        scope.deinit(env.ctx);
        gpa.destroy(scope);
    }
    return env.ok();
}

pub export fn napi_open_escapable_handle_scope(env_: napi_env, result: ?*napi_escapable_handle_scope) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const scope = HandleScope.init(true);
    env.scope_stack.append(gpa, scope) catch return env.genericFailure();
    r.* = scope;
    return env.ok();
}

pub export fn napi_close_escapable_handle_scope(env_: napi_env, scope_: napi_escapable_handle_scope) callconv(.c) napi_status {
    return napi_close_handle_scope(env_, scope_);
}

pub export fn napi_escape_handle(env_: napi_env, scope_: napi_escapable_handle_scope, escapee: napi_value, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const scope = scope_ orelse return env.invalidArg();
    const r = result orelse return env.invalidArg();
    if (!scope.escapable) return env.invalidArg();
    if (scope.escaped) return env.setLastError(.escape_called_twice);
    scope.escaped = true;

    const val = toVal(escapee);
    // Move the value to the parent scope
    if (env.scope_stack.items.len >= 2) {
        const parent = env.scope_stack.items[env.scope_stack.items.len - 2];
        r.* = parent.track(env.ctx, val);
    } else {
        r.* = env.createNapiValue(val);
    }
    return env.ok();
}

// ──────────────────── Error Handling ────────────────────

pub export fn napi_throw(env_: napi_env, @"error": napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const val = toVal(@"error");
    _ = qjs.JS_Throw(env.ctx, qjs.JS_DupValue(env.ctx, val));
    env.setPendingException(val);
    return env.ok();
}

pub export fn napi_throw_error(env_: napi_env, code: [*c]const u8, msg: [*c]const u8) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    _ = code;
    const err = qjs.JS_ThrowInternalError(env.ctx, msg orelse "Unknown error");
    _ = err;
    return env.ok();
}

pub export fn napi_throw_type_error(env_: napi_env, code: [*c]const u8, msg: [*c]const u8) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    _ = code;
    _ = qjs.JS_ThrowTypeError(env.ctx, msg orelse "Type error");
    return env.ok();
}

pub export fn napi_throw_range_error(env_: napi_env, code: [*c]const u8, msg: [*c]const u8) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    _ = code;
    _ = qjs.JS_ThrowRangeError(env.ctx, msg orelse "Range error");
    return env.ok();
}

pub export fn napi_create_error(env_: napi_env, code: napi_value, msg: napi_value, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    _ = code;
    const msg_val = toVal(msg);
    const err = qjs.JS_NewError(env.ctx);
    if (!js.js_is_exception(err)) {
        _ = qjs.JS_SetPropertyStr(env.ctx, err, "message", qjs.JS_DupValue(env.ctx, msg_val));
    }
    r.* = env.createNapiValue(err);
    return env.ok();
}

pub export fn napi_create_type_error(env_: napi_env, code: napi_value, msg: napi_value, result: ?*napi_value) callconv(.c) napi_status {
    return napi_create_error(env_, code, msg, result);
}

pub export fn napi_create_range_error(env_: napi_env, code: napi_value, msg: napi_value, result: ?*napi_value) callconv(.c) napi_status {
    return napi_create_error(env_, code, msg, result);
}

pub export fn napi_is_exception_pending(env_: napi_env, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = env.has_pending_exception;
    return env.ok();
}

pub export fn napi_get_and_clear_last_exception(env_: napi_env, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    if (env.has_pending_exception) {
        r.* = env.createNapiValue(env.pending_exception);
        env.clearPendingException();
    } else {
        r.* = env.createNapiValue(js.js_undefined());
    }
    return env.ok();
}

pub export fn napi_get_last_error_info(env_: napi_env, result: ?*[*c]const nt.napi_extended_error_info) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = &env.last_error;
    return @intFromEnum(Status.ok);
}

// ──────────────────── Function Creation & Calling ────────────────────

pub export fn napi_create_function(
    env_: napi_env,
    utf8name: [*c]const u8,
    length: usize,
    cb: napi_callback,
    data: ?*anyopaque,
    result: ?*napi_value,
) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const callback = cb orelse return env.invalidArg();
    _ = length;

    const cbd = createCallbackData(env, callback, data) orelse return env.genericFailure();

    const ptr_as_int: i64 = @bitCast(@intFromPtr(cbd));
    var func_data = [_]qjs.JSValue{qjs.JS_NewInt64(env.ctx, ptr_as_int)};
    const func = qjs.JS_NewCFunctionData(env.ctx, &napiCallbackTrampoline, 0, 0, 1, &func_data);
    if (js.js_is_exception(func)) return env.genericFailure();

    if (utf8name != null) {
        const name_val = qjs.JS_NewString(env.ctx, utf8name);
        _ = qjs.JS_DefinePropertyValueStr(env.ctx, func, "name", name_val, 0);
    }

    r.* = env.createNapiValue(func);
    return env.ok();
}

fn napiCallbackTrampoline(
    ctx: ?*qjs.JSContext,
    this_val: qjs.JSValue,
    argc: c_int,
    argv: [*c]qjs.JSValue,
    _: c_int,
    func_data: [*c]qjs.JSValue,
) callconv(.c) qjs.JSValue {
    var ptr_int: i64 = 0;
    _ = qjs.JS_ToInt64(ctx, &ptr_int, func_data[0]);
    const cbd: *FunctionCallbackData = @ptrFromInt(@as(usize, @bitCast(ptr_int)));

    // Detect constructor call: if this_val is a function (new.target),
    // create a proper instance with the prototype chain.
    var effective_this = this_val;
    var is_constructor_call = false;
    if (qjs.JS_IsFunction(ctx, this_val)) {
        // this_val is new.target — create instance from its prototype
        const proto = qjs.JS_GetPropertyStr(ctx, this_val, "prototype");
        if (qjs.JS_IsObject(proto)) {
            effective_this = qjs.JS_NewObjectProtoClass(ctx, proto, 0);
            is_constructor_call = true;
        }
        qjs.JS_FreeValue(ctx, proto);
    }

    var info = CallbackInfo{
        .this = effective_this,
        .args = argv,
        .argc = argc,
        .data = cbd.data,
        .new_target = if (is_constructor_call) this_val else js.js_undefined(),
    };

    const was_in_callback = cbd.env.in_callback;
    cbd.env.in_callback = true;
    const napi_result = cbd.cb(cbd.env, &info);
    cbd.env.in_callback = was_in_callback;

    // For constructor calls, return the instance (not the napi_value)
    if (is_constructor_call) {
        if (napi_result) |_| {
            // Addon returned a value — for constructors this is typically undefined
            // in N-API; the instance is the `this` object
        }
        return effective_this;
    }
    return toVal(napi_result);
}

pub export fn napi_call_function(
    env_: napi_env,
    recv: napi_value,
    func: napi_value,
    argc: usize,
    argv: [*c]const napi_value,
    result: ?*napi_value,
) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const func_val = toVal(func);
    if (!qjs.JS_IsFunction(env.ctx, func_val)) return env.setLastError(.function_expected);

    const this_val = toVal(recv);

    // Convert napi_value array to JSValue array
    var js_args_buf: [64]qjs.JSValue = undefined;
    const js_argc = @min(argc, 64);
    for (0..js_argc) |i| {
        js_args_buf[i] = if (argv != null) toVal(argv[i]) else js.js_undefined();
    }

    const ret = qjs.JS_Call(env.ctx, func_val, this_val, @intCast(js_argc), if (js_argc > 0) &js_args_buf else null);
    if (js.js_is_exception(ret)) {
        const exc = qjs.JS_GetException(env.ctx);
        env.setPendingException(exc);
        qjs.JS_FreeValue(env.ctx, exc);
        return env.setLastError(.pending_exception);
    }

    if (result) |r| {
        r.* = env.createNapiValue(ret);
    }
    qjs.JS_FreeValue(env.ctx, ret);
    return env.ok();
}

pub export fn napi_get_cb_info(
    env_: napi_env,
    cbinfo_: napi_callback_info,
    argc: ?*usize,
    argv: ?[*]napi_value,
    this_arg: ?*napi_value,
    data: ?*?*anyopaque,
) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const info: *CallbackInfo = cbinfo_ orelse return env.invalidArg();

    if (argc) |ac| {
        const requested = ac.*;
        const available: usize = @intCast(@max(info.argc, 0));
        const copy_count = @min(requested, available);

        if (argv) |av| {
            for (0..copy_count) |i| {
                av[i] = env.createNapiValue(info.args[i]);
            }
            // Fill remaining with undefined
            for (copy_count..requested) |i| {
                av[i] = env.createNapiValue(js.js_undefined());
            }
        }
        ac.* = available;
    }

    if (this_arg) |t| {
        t.* = env.createNapiValue(info.this);
    }

    if (data) |d| {
        d.* = info.data;
    }

    return env.ok();
}

pub export fn napi_get_new_target(env_: napi_env, cbinfo_: napi_callback_info, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const info: *CallbackInfo = cbinfo_ orelse return env.invalidArg();
    const r = result orelse return env.invalidArg();
    r.* = env.createNapiValue(info.new_target);
    return env.ok();
}

// ──────────────────── Instanceof ────────────────────

pub export fn napi_instanceof(env_: napi_env, object: napi_value, constructor: napi_value, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const ret = qjs.JS_IsInstanceOf(env.ctx, toVal(object), toVal(constructor));
    if (ret < 0) return env.setLastError(.pending_exception);
    r.* = ret != 0;
    return env.ok();
}

// ──────────────────── References ────────────────────

pub export fn napi_create_reference(env_: napi_env, value: napi_value, initial_refcount: u32, result: ?*napi_ref) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);

    const ref_obj = gpa.create(NapiReference) catch return env.genericFailure();
    ref_obj.* = .{
        .value = qjs.JS_DupValue(env.ctx, val),
        .ref_count = initial_refcount,
        .ctx = env.ctx,
    };
    env.refs.append(gpa, ref_obj) catch {
        ref_obj.deinit();
        return env.genericFailure();
    };
    r.* = ref_obj;
    return env.ok();
}

pub export fn napi_delete_reference(env_: napi_env, ref_: napi_ref) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const ref_obj: *NapiReference = ref_ orelse return env.invalidArg();
    // Remove from env tracking
    for (env.refs.items, 0..) |r, i| {
        if (r == ref_obj) {
            _ = env.refs.swapRemove(i);
            break;
        }
    }
    ref_obj.deinit();
    return env.ok();
}

pub export fn napi_reference_ref(env_: napi_env, ref_: napi_ref, result: ?*u32) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const ref_obj: *NapiReference = ref_ orelse return env.invalidArg();
    ref_obj.ref();
    if (result) |r| r.* = ref_obj.ref_count;
    return env.ok();
}

pub export fn napi_reference_unref(env_: napi_env, ref_: napi_ref, result: ?*u32) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const ref_obj: *NapiReference = ref_ orelse return env.invalidArg();
    ref_obj.unref();
    if (result) |r| r.* = ref_obj.ref_count;
    return env.ok();
}

pub export fn napi_get_reference_value(env_: napi_env, ref_: napi_ref, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const ref_obj: *NapiReference = ref_ orelse return env.invalidArg();
    const r = result orelse return env.invalidArg();
    r.* = env.createNapiValue(ref_obj.value);
    return env.ok();
}

// ──────────────────── Promises ────────────────────

pub export fn napi_create_promise(env_: napi_env, deferred_: ?*napi_deferred, promise_: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const deferred_out = deferred_ orelse return env.invalidArg();
    const promise_out = promise_ orelse return env.invalidArg();

    var resolve_funcs: [2]qjs.JSValue = undefined;
    const promise = qjs.JS_NewPromiseCapability(env.ctx, &resolve_funcs);
    if (js.js_is_exception(promise)) return env.genericFailure();

    const d = gpa.create(Deferred) catch {
        qjs.JS_FreeValue(env.ctx, promise);
        qjs.JS_FreeValue(env.ctx, resolve_funcs[0]);
        qjs.JS_FreeValue(env.ctx, resolve_funcs[1]);
        return env.genericFailure();
    };
    d.* = .{
        .resolve_func = resolve_funcs[0],
        .reject_func = resolve_funcs[1],
        .ctx = env.ctx,
    };

    deferred_out.* = d;
    promise_out.* = env.createNapiValue(promise);
    return env.ok();
}

pub export fn napi_resolve_deferred(env_: napi_env, deferred_: napi_deferred, resolution: napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const d: *Deferred = deferred_ orelse return env.invalidArg();
    const val = toVal(resolution);
    var args = [_]qjs.JSValue{val};
    const ret = qjs.JS_Call(env.ctx, d.resolve_func, js.js_undefined(), 1, &args);
    qjs.JS_FreeValue(env.ctx, ret);
    d.deinit();
    return env.ok();
}

pub export fn napi_reject_deferred(env_: napi_env, deferred_: napi_deferred, rejection: napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const d: *Deferred = deferred_ orelse return env.invalidArg();
    const val = toVal(rejection);
    var args = [_]qjs.JSValue{val};
    const ret = qjs.JS_Call(env.ctx, d.reject_func, js.js_undefined(), 1, &args);
    qjs.JS_FreeValue(env.ctx, ret);
    d.deinit();
    return env.ok();
}

// ──────────────────── Run Script ────────────────────

pub export fn napi_run_script(env_: napi_env, script: napi_value, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const script_val = toVal(script);
    if (!qjs.JS_IsString(script_val)) return env.setLastError(.string_expected);

    var len: usize = 0;
    const cstr = qjs.JS_ToCStringLen(env.ctx, &len, script_val);
    if (cstr == null) return env.genericFailure();
    defer qjs.JS_FreeCString(env.ctx, cstr);

    const val = qjs.JS_Eval(env.ctx, cstr, len, "<napi>", qjs.JS_EVAL_TYPE_GLOBAL);
    if (js.js_is_exception(val)) {
        const exc = qjs.JS_GetException(env.ctx);
        env.setPendingException(exc);
        qjs.JS_FreeValue(env.ctx, exc);
        return env.setLastError(.pending_exception);
    }
    r.* = env.createNapiValue(val);
    return env.ok();
}

// ──────────────────── Version ────────────────────

pub export fn napi_get_version(env_: napi_env, result: ?*u32) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = nt.NAPI_VERSION;
    return env.ok();
}

const node_version = nt.napi_node_version{
    .major = 22,
    .minor = 0,
    .patch = 0,
    .release = "quickbeam",
};

pub export fn napi_get_node_version(env_: napi_env, result: ?*[*c]const nt.napi_node_version) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    r.* = &node_version;
    return env.ok();
}

// ──────────────────── Instance Data ────────────────────

pub export fn napi_set_instance_data(env_: napi_env, data: ?*anyopaque, finalize_cb: napi_finalize, finalize_hint: ?*anyopaque) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    env.instance_data = data;
    env.instance_data_finalize = finalize_cb;
    env.instance_data_hint = finalize_hint;
    return env.ok();
}

pub export fn napi_get_instance_data(env_: napi_env, data: ?*?*anyopaque) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const d = data orelse return env.invalidArg();
    d.* = env.instance_data;
    return env.ok();
}

// ──────────────────── Wrap / Unwrap ────────────────────

// ──────────────────── External ────────────────────

pub export fn napi_create_external(
    env_: napi_env,
    data: ?*anyopaque,
    finalize_cb: napi_finalize,
    finalize_hint: ?*anyopaque,
    result: ?*napi_value,
) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();

    const ext_data: *ExternalData = @ptrCast(@alignCast(qjs.js_mallocz(env.ctx, @sizeOf(ExternalData)) orelse return env.genericFailure()));
    ext_data.* = .{
        .data = data,
        .finalize_cb = finalize_cb,
        .finalize_hint = finalize_hint,
    };

    const obj = qjs.JS_NewObjectClass(env.ctx, @intCast(nt.external_class_id));
    if (js.js_is_exception(obj)) return env.genericFailure();
    _ = qjs.JS_SetOpaque(obj, ext_data);

    r.* = env.createNapiValue(obj);
    return env.ok();
}

pub export fn napi_get_value_external(env_: napi_env, value: napi_value, result: ?*?*anyopaque) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);
    const ext_data: ?*ExternalData = @ptrCast(@alignCast(qjs.JS_GetOpaque(val, nt.external_class_id)));
    r.* = if (ext_data) |e| e.data else null;
    return env.ok();
}

// ──────────────────── Async stubs ────────────────────

pub export fn napi_async_init(_: napi_env, _: napi_value, _: napi_value, result: ?*?*anyopaque) callconv(.c) napi_status {
    if (result) |r| r.* = null;
    return @intFromEnum(Status.ok);
}

pub export fn napi_async_destroy(_: napi_env, _: ?*anyopaque) callconv(.c) napi_status {
    return @intFromEnum(Status.ok);
}

pub export fn napi_make_callback(env_: napi_env, _: ?*anyopaque, recv: napi_value, func: napi_value, argc: usize, argv: [*c]const napi_value, result: ?*napi_value) callconv(.c) napi_status {
    return napi_call_function(env_, recv, func, argc, argv, result);
}

pub export fn napi_open_callback_scope(_: napi_env, _: napi_value, _: ?*anyopaque, _: ?*anyopaque) callconv(.c) napi_status {
    return @intFromEnum(Status.ok);
}

pub export fn napi_close_callback_scope(_: napi_env, _: ?*anyopaque) callconv(.c) napi_status {
    return @intFromEnum(Status.ok);
}

// ──────────────────── Object freeze/seal ────────────────────

pub export fn napi_object_freeze(env_: napi_env, object: napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);
    // Call Object.freeze via eval
    const global = qjs.JS_GetGlobalObject(env.ctx);
    defer qjs.JS_FreeValue(env.ctx, global);
    const object_ctor = qjs.JS_GetPropertyStr(env.ctx, global, "Object");
    defer qjs.JS_FreeValue(env.ctx, object_ctor);
    const freeze_fn = qjs.JS_GetPropertyStr(env.ctx, object_ctor, "freeze");
    defer qjs.JS_FreeValue(env.ctx, freeze_fn);
    var args = [_]qjs.JSValue{obj};
    const ret = qjs.JS_Call(env.ctx, freeze_fn, object_ctor, 1, &args);
    qjs.JS_FreeValue(env.ctx, ret);
    return env.ok();
}

pub export fn napi_object_seal(env_: napi_env, object: napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);
    const global = qjs.JS_GetGlobalObject(env.ctx);
    defer qjs.JS_FreeValue(env.ctx, global);
    const object_ctor = qjs.JS_GetPropertyStr(env.ctx, global, "Object");
    defer qjs.JS_FreeValue(env.ctx, object_ctor);
    const seal_fn = qjs.JS_GetPropertyStr(env.ctx, object_ctor, "seal");
    defer qjs.JS_FreeValue(env.ctx, seal_fn);
    var args = [_]qjs.JSValue{obj};
    const ret = qjs.JS_Call(env.ctx, seal_fn, object_ctor, 1, &args);
    qjs.JS_FreeValue(env.ctx, ret);
    return env.ok();
}

// ──────────────────── Misc stubs ────────────────────

pub export fn napi_adjust_external_memory(_: napi_env, _: i64, _: ?*i64) callconv(.c) napi_status {
    return @intFromEnum(Status.ok);
}

pub export fn napi_add_env_cleanup_hook(_: napi_env, _: ?*const fn (?*anyopaque) callconv(.c) void, _: ?*anyopaque) callconv(.c) napi_status {
    return @intFromEnum(Status.ok);
}

pub export fn napi_remove_env_cleanup_hook(_: napi_env, _: ?*const fn (?*anyopaque) callconv(.c) void, _: ?*anyopaque) callconv(.c) napi_status {
    return @intFromEnum(Status.ok);
}

pub export fn napi_add_async_cleanup_hook(_: napi_env, _: ?*const fn (?*anyopaque, ?*anyopaque) callconv(.c) void, _: ?*anyopaque, _: ?*?*anyopaque) callconv(.c) napi_status {
    return @intFromEnum(Status.ok);
}

pub export fn napi_remove_async_cleanup_hook(_: ?*anyopaque) callconv(.c) napi_status {
    return @intFromEnum(Status.ok);
}

pub export fn napi_type_tag_object(_: napi_env, _: napi_value, _: [*c]const nt.napi_type_tag) callconv(.c) napi_status {
    return @intFromEnum(Status.ok);
}

pub export fn napi_check_object_type_tag(_: napi_env, _: napi_value, _: [*c]const nt.napi_type_tag, result: ?*bool) callconv(.c) napi_status {
    if (result) |r| r.* = false;
    return @intFromEnum(Status.ok);
}

pub export fn napi_fatal_error(_: ?[*:0]const u8, _: usize, msg_ptr: ?[*:0]const u8, _: usize) callconv(.c) noreturn {
    const msg = if (msg_ptr) |p| std.mem.span(p) else "fatal error";
    std.debug.panic("NAPI FATAL ERROR: {s}", .{msg});
}

pub export fn napi_fatal_exception(env_: napi_env, err: napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    env.setPendingException(toVal(err));
    return env.ok();
}

// ──────────────────── Module registration ────────────────────

var pending_napi_module: ?*nt.napi_module = null;

pub export fn napi_module_register(mod: ?*nt.napi_module) callconv(.c) void {
    pending_napi_module = mod;
}

pub fn getPendingModule() ?*nt.napi_module {
    return pending_napi_module;
}

pub fn clearPendingModule() void {
    pending_napi_module = null;
}

// ──────────────────── Async Work ────────────────────

fn asyncWorkRunner(work: *AsyncWork) void {
    work.status.store(.started, .release);
    work.execute(work.env, work.data);

    const final_status: AsyncWork.AsyncStatus = if (work.status.cmpxchgStrong(.started, .completed, .seq_cst, .seq_cst) == null)
        .completed
    else
        .cancelled;
    _ = final_status;

    // Dispatch completion back to the worker thread
    if (work.rd) |rd| {
        types.enqueue(rd, .{ .napi_async_complete = .{ .work = work } });
    }
}

// ──────────────────── ArrayBuffer ────────────────────

pub export fn napi_create_arraybuffer(env_: napi_env, byte_length: usize, data: ?*?[*]u8, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();

    const buf = gpa.alloc(u8, byte_length) catch return env.genericFailure();
    @memset(buf, 0);

    const ab = qjs.JS_NewArrayBuffer(env.ctx, buf.ptr, byte_length, &buffers.arrayBufferFree, @ptrFromInt(byte_length), false);
    if (js.js_is_exception(ab)) {
        gpa.free(buf);
        return env.genericFailure();
    }

    if (data) |d| d.* = buf.ptr;
    r.* = env.createNapiValue(ab);
    return env.ok();
}

pub export fn napi_get_arraybuffer_info(env_: napi_env, arraybuffer: napi_value, data: ?*?[*]u8, byte_length: ?*usize) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const val = toVal(arraybuffer);
    var size: usize = 0;
    const ptr = qjs.JS_GetArrayBuffer(env.ctx, &size, val);
    if (data) |d| d.* = ptr;
    if (byte_length) |bl| bl.* = size;
    return env.ok();
}

pub export fn napi_detach_arraybuffer(env_: napi_env, arraybuffer: napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    qjs.JS_DetachArrayBuffer(env.ctx, toVal(arraybuffer));
    return env.ok();
}

pub export fn napi_is_detached_arraybuffer(env_: napi_env, value: napi_value, result: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);
    if (!qjs.JS_IsArrayBuffer(val)) {
        r.* = false;
        return env.ok();
    }
    var size: usize = 0;
    const ptr = qjs.JS_GetArrayBuffer(env.ctx, &size, val);
    r.* = ptr == null and size == 0;
    return env.ok();
}

// ──────────────────── Buffer (Node.js Buffer ≈ Uint8Array) ────────────────────

// ──────────────────── Date ────────────────────

pub export fn napi_create_date(env_: napi_env, time: f64, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const date = qjs.JS_NewDate(env.ctx, time);
    if (js.js_is_exception(date)) return env.genericFailure();
    r.* = env.createNapiValue(date);
    return env.ok();
}

pub export fn napi_get_date_value(env_: napi_env, value: napi_value, result: ?*f64) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const val = toVal(value);
    // Call getTime() on the Date object
    const get_time = qjs.JS_GetPropertyStr(env.ctx, val, "getTime");
    defer qjs.JS_FreeValue(env.ctx, get_time);
    if (!qjs.JS_IsFunction(env.ctx, get_time)) return env.setLastError(.date_expected);
    const time_val = qjs.JS_Call(env.ctx, get_time, val, 0, null);
    defer qjs.JS_FreeValue(env.ctx, time_val);
    if (js.js_is_exception(time_val)) return env.setLastError(.pending_exception);
    var d: f64 = 0;
    if (qjs.JS_ToFloat64(env.ctx, &d, time_val) < 0) return env.genericFailure();
    r.* = d;
    return env.ok();
}

// ──────────────────── BigInt ────────────────────

pub export fn napi_create_bigint_int64(env_: napi_env, value: i64, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const bi = qjs.JS_NewBigInt64(env.ctx, value);
    r.* = env.createNapiValue(bi);
    return env.ok();
}

pub export fn napi_create_bigint_uint64(env_: napi_env, value: u64, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const bi = qjs.JS_NewBigUint64(env.ctx, value);
    r.* = env.createNapiValue(bi);
    return env.ok();
}

pub export fn napi_get_value_bigint_int64(env_: napi_env, value: napi_value, result: ?*i64, lossless: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    var i: i64 = 0;
    if (qjs.JS_ToInt64(env.ctx, &i, toVal(value)) < 0) return env.setLastError(.bigint_expected);
    r.* = i;
    if (lossless) |l| l.* = true;
    return env.ok();
}

pub export fn napi_get_value_bigint_uint64(env_: napi_env, value: napi_value, result: ?*u64, lossless: ?*bool) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    var i: i64 = 0;
    if (qjs.JS_ToInt64(env.ctx, &i, toVal(value)) < 0) return env.setLastError(.bigint_expected);
    r.* = @bitCast(i);
    if (lossless) |l| l.* = true;
    return env.ok();
}

// ──────────────────── Property Definition ────────────────────

pub export fn napi_define_properties(env_: napi_env, object: napi_value, property_count: usize, properties: [*c]const napi_property_descriptor) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);

    for (0..property_count) |i| {
        const prop = properties[i];
        const name_atom: qjs.JSAtom = if (prop.utf8name != null)
            qjs.JS_NewAtom(env.ctx, prop.utf8name)
        else if (prop.name) |n|
            qjs.JS_ValueToAtom(env.ctx, n.*)
        else
            continue;
        defer qjs.JS_FreeAtom(env.ctx, name_atom);

        var flags: c_int = 0;
        if (prop.attributes & nt.NAPI_WRITABLE != 0) flags |= qjs.JS_PROP_WRITABLE;
        if (prop.attributes & nt.NAPI_ENUMERABLE != 0) flags |= qjs.JS_PROP_ENUMERABLE;
        if (prop.attributes & nt.NAPI_CONFIGURABLE != 0) flags |= qjs.JS_PROP_CONFIGURABLE;

        if (prop.method) |method| {
            _ = qjs.JS_DefinePropertyValue(env.ctx, obj, name_atom, createJsFunction(env, method, prop.data), flags | qjs.JS_PROP_HAS_VALUE);
        } else if (prop.getter != null or prop.setter != null) {
            const getter_val = if (prop.getter) |g| createJsFunction(env, g, prop.data) else js.js_undefined();
            const setter_val = if (prop.setter) |s| createJsFunction(env, s, prop.data) else js.js_undefined();
            _ = qjs.JS_DefinePropertyGetSet(env.ctx, obj, name_atom, getter_val, setter_val, flags | qjs.JS_PROP_HAS_GET | qjs.JS_PROP_HAS_SET);
        } else if (prop.value) |v| {
            _ = qjs.JS_DefinePropertyValue(env.ctx, obj, name_atom, qjs.JS_DupValue(env.ctx, v.*), flags | qjs.JS_PROP_HAS_VALUE);
        }
    }
    return env.ok();
}

pub export fn napi_get_property_names(env_: napi_env, object: napi_value, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const obj = toVal(object);
    if (!qjs.JS_IsObject(obj)) return env.setLastError(.object_expected);

    var ptab: [*c]qjs.JSPropertyEnum = null;
    var plen: u32 = 0;
    if (qjs.JS_GetOwnPropertyNames(env.ctx, &ptab, &plen, obj, qjs.JS_GPN_STRING_MASK | qjs.JS_GPN_ENUM_ONLY) < 0)
        return env.genericFailure();
    defer {
        for (0..plen) |i| qjs.JS_FreeAtom(env.ctx, ptab[i].atom);
        qjs.js_free(env.ctx, ptab);
    }

    const arr = qjs.JS_NewArray(env.ctx);
    for (0..plen) |i| {
        const name_val = qjs.JS_AtomToString(env.ctx, ptab[i].atom);
        _ = qjs.JS_SetPropertyUint32(env.ctx, arr, @intCast(i), name_val);
    }
    r.* = env.createNapiValue(arr);
    return env.ok();
}

pub export fn napi_get_all_property_names(
    env_: napi_env,
    object: napi_value,
    _: c_uint, // collection_mode
    _: c_uint, // filter
    _: c_uint, // conversion
    result: ?*napi_value,
) callconv(.c) napi_status {
    return napi_get_property_names(env_, object, result);
}

// ──────────────────── New Instance ────────────────────

pub export fn napi_new_instance(env_: napi_env, constructor: napi_value, argc: usize, argv: ?[*]const napi_value, result: ?*napi_value) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const ctor = toVal(constructor);
    if (!qjs.JS_IsFunction(env.ctx, ctor)) return env.setLastError(.function_expected);

    var js_args_buf: [64]qjs.JSValue = undefined;
    const js_argc = @min(argc, 64);
    for (0..js_argc) |i| {
        js_args_buf[i] = if (argv) |a| toVal(a[i]) else js.js_undefined();
    }

    const global = qjs.JS_GetGlobalObject(env.ctx);
    defer qjs.JS_FreeValue(env.ctx, global);

    // Use JS_CallConstructor for new operator semantics
    const ret = qjs.JS_CallConstructor(env.ctx, ctor, @intCast(js_argc), if (js_argc > 0) &js_args_buf else null);
    if (js.js_is_exception(ret)) {
        const exc = qjs.JS_GetException(env.ctx);
        env.setPendingException(exc);
        qjs.JS_FreeValue(env.ctx, exc);
        return env.setLastError(.pending_exception);
    }
    r.* = env.createNapiValue(ret);
    return env.ok();
}

// ──────────────────── Define Class ────────────────────

pub export fn napi_define_class(
    env_: napi_env,
    utf8name: [*c]const u8,
    length: usize,
    constructor_cb: napi_callback,
    cb_data: ?*anyopaque,
    property_count: usize,
    properties: [*c]const napi_property_descriptor,
    result: ?*napi_value,
) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const ctor_fn = constructor_cb orelse return env.invalidArg();
    _ = length;

    // Create the constructor function
    const ctor = createJsFunction(env, ctor_fn, cb_data);
    if (js.js_is_exception(ctor)) return env.genericFailure();

    // Mark as constructor
    _ = qjs.JS_SetConstructorBit(env.ctx, ctor, true);

    if (utf8name != null) {
        const name_val = qjs.JS_NewString(env.ctx, utf8name);
        _ = qjs.JS_DefinePropertyValueStr(env.ctx, ctor, "name", name_val, 0);
    }

    // Create prototype object and wire up ctor <-> proto
    const proto = qjs.JS_NewObject(env.ctx);
    _ = qjs.JS_SetConstructor(env.ctx, ctor, proto);

    // Add properties — static ones go on the constructor, instance ones on the prototype
    for (0..property_count) |i| {
        const prop = properties[i];
        const target = if (prop.attributes & nt.NAPI_STATIC != 0) ctor else proto;

        const name_atom: qjs.JSAtom = if (prop.utf8name != null)
            qjs.JS_NewAtom(env.ctx, prop.utf8name)
        else if (prop.name) |n|
            qjs.JS_ValueToAtom(env.ctx, n.*)
        else
            continue;
        defer qjs.JS_FreeAtom(env.ctx, name_atom);

        var flags: c_int = 0;
        if (prop.attributes & nt.NAPI_WRITABLE != 0) flags |= qjs.JS_PROP_WRITABLE;
        if (prop.attributes & nt.NAPI_ENUMERABLE != 0) flags |= qjs.JS_PROP_ENUMERABLE;
        if (prop.attributes & nt.NAPI_CONFIGURABLE != 0) flags |= qjs.JS_PROP_CONFIGURABLE;

        if (prop.method) |method| {
            _ = qjs.JS_DefinePropertyValue(env.ctx, target, name_atom, createJsFunction(env, method, prop.data), flags | qjs.JS_PROP_HAS_VALUE);
        } else if (prop.getter != null or prop.setter != null) {
            const getter_val = if (prop.getter) |g| createJsFunction(env, g, prop.data) else js.js_undefined();
            const setter_val = if (prop.setter) |s| createJsFunction(env, s, prop.data) else js.js_undefined();
            _ = qjs.JS_DefinePropertyGetSet(env.ctx, target, name_atom, getter_val, setter_val, flags | qjs.JS_PROP_HAS_GET | qjs.JS_PROP_HAS_SET);
        } else if (prop.value) |v| {
            _ = qjs.JS_DefinePropertyValue(env.ctx, target, name_atom, qjs.JS_DupValue(env.ctx, v.*), flags | qjs.JS_PROP_HAS_VALUE);
        }
    }

    qjs.JS_FreeValue(env.ctx, proto);

    r.* = env.createNapiValue(ctor);
    return env.ok();
}

// ──────────────────── TypedArray ────────────────────

pub export fn napi_create_typedarray(
    env_: napi_env,
    array_type: napi_typedarray_type,
    length: usize,
    arraybuffer: napi_value,
    byte_offset: usize,
    result: ?*napi_value,
) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();
    const ab = toVal(arraybuffer);

    // Get the TypedArray constructor name
    const ctor_name: [*:0]const u8 = switch (array_type) {
        .int8_array => "Int8Array",
        .uint8_array => "Uint8Array",
        .uint8_clamped_array => "Uint8ClampedArray",
        .int16_array => "Int16Array",
        .uint16_array => "Uint16Array",
        .int32_array => "Int32Array",
        .uint32_array => "Uint32Array",
        .float32_array => "Float32Array",
        .float64_array => "Float64Array",
        .bigint64_array => "BigInt64Array",
        .biguint64_array => "BigUint64Array",
    };

    // Get the constructor from global
    const global = qjs.JS_GetGlobalObject(env.ctx);
    defer qjs.JS_FreeValue(env.ctx, global);
    const ctor = qjs.JS_GetPropertyStr(env.ctx, global, ctor_name);
    defer qjs.JS_FreeValue(env.ctx, ctor);

    // new TypedArray(buffer, byteOffset, length)
    var args = [_]qjs.JSValue{
        ab,
        qjs.JS_NewUint32(env.ctx, @intCast(byte_offset)),
        qjs.JS_NewUint32(env.ctx, @intCast(length)),
    };
    const ta = qjs.JS_CallConstructor(env.ctx, ctor, 3, &args);
    if (js.js_is_exception(ta)) return env.setLastError(.pending_exception);

    r.* = env.createNapiValue(ta);
    return env.ok();
}

pub export fn napi_get_typedarray_info(
    env_: napi_env,
    typedarray: napi_value,
    maybe_type: ?*napi_typedarray_type,
    maybe_length: ?*usize,
    maybe_data: ?*?[*]u8,
    maybe_arraybuffer: ?*napi_value,
    maybe_byte_offset: ?*usize,
) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const val = toVal(typedarray);

    var poffset: usize = 0;
    var psize: usize = 0;
    const ab = qjs.JS_GetTypedArrayBuffer(env.ctx, val, &poffset, &psize, null);
    if (js.js_is_exception(ab)) return env.genericFailure();
    defer qjs.JS_FreeValue(env.ctx, ab);

    if (maybe_arraybuffer) |mab| {
        mab.* = env.createNapiValue(ab);
    }
    if (maybe_byte_offset) |bo| bo.* = poffset;

    // Get buffer data pointer
    if (maybe_data) |md| {
        var ab_size: usize = 0;
        const ptr = qjs.JS_GetArrayBuffer(env.ctx, &ab_size, ab);
        if (ptr != null) {
            md.* = ptr + poffset;
        } else {
            md.* = null;
        }
    }

    // Get element count from JS length property
    if (maybe_length) |ml| {
        const len_val = qjs.JS_GetPropertyStr(env.ctx, val, "length");
        defer qjs.JS_FreeValue(env.ctx, len_val);
        var len: u32 = 0;
        _ = qjs.JS_ToUint32(env.ctx, &len, len_val);
        ml.* = len;
    }

    // Determine typed array type from constructor name
    if (maybe_type) |mt| {
        const ctor = qjs.JS_GetPropertyStr(env.ctx, val, "constructor");
        defer qjs.JS_FreeValue(env.ctx, ctor);
        const name = qjs.JS_GetPropertyStr(env.ctx, ctor, "name");
        defer qjs.JS_FreeValue(env.ctx, name);
        if (qjs.JS_IsString(name)) {
            var slen: usize = 0;
            const cstr = qjs.JS_ToCStringLen(env.ctx, &slen, name);
            if (cstr != null) {
                defer qjs.JS_FreeCString(env.ctx, cstr);
                const s = cstr[0..slen];
                mt.* = if (std.mem.eql(u8, s, "Int8Array")) .int8_array else if (std.mem.eql(u8, s, "Uint8Array")) .uint8_array else if (std.mem.eql(u8, s, "Uint8ClampedArray")) .uint8_clamped_array else if (std.mem.eql(u8, s, "Int16Array")) .int16_array else if (std.mem.eql(u8, s, "Uint16Array")) .uint16_array else if (std.mem.eql(u8, s, "Int32Array")) .int32_array else if (std.mem.eql(u8, s, "Uint32Array")) .uint32_array else if (std.mem.eql(u8, s, "Float32Array")) .float32_array else if (std.mem.eql(u8, s, "Float64Array")) .float64_array else if (std.mem.eql(u8, s, "BigInt64Array")) .bigint64_array else if (std.mem.eql(u8, s, "BigUint64Array")) .biguint64_array else .uint8_array;
            }
        }
    }
    return env.ok();
}

pub export fn napi_create_external_arraybuffer(
    env_: napi_env,
    external_data: ?*anyopaque,
    byte_length: usize,
    finalize_cb: napi_finalize,
    finalize_hint: ?*anyopaque,
    result: ?*napi_value,
) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();

    const ext_wrap = gpa.create(ExternalAbData) catch return env.genericFailure();
    ext_wrap.* = .{ .env = env, .finalize_cb = finalize_cb, .finalize_hint = finalize_hint };

    const ptr: [*]u8 = if (external_data) |ed| @ptrCast(ed) else @ptrFromInt(1);
    const ab = qjs.JS_NewArrayBuffer(env.ctx, ptr, byte_length, &externalAbFree, @ptrCast(ext_wrap), false);
    if (js.js_is_exception(ab)) {
        gpa.destroy(ext_wrap);
        return env.genericFailure();
    }

    r.* = env.createNapiValue(ab);
    return env.ok();
}

const ExternalAbData = struct {
    env: *NapiEnv,
    finalize_cb: napi_finalize,
    finalize_hint: ?*anyopaque,
};

fn externalAbFree(_: ?*qjs.JSRuntime, user_data: ?*anyopaque, ptr: ?*anyopaque) callconv(.c) void {
    const ext_wrap: *ExternalAbData = @ptrCast(@alignCast(user_data orelse return));
    if (ext_wrap.finalize_cb) |cb| {
        cb(ext_wrap.env, ptr, ext_wrap.finalize_hint);
    }
    gpa.destroy(ext_wrap);
}

// ──────────────────── DataView ────────────────────

pub export fn napi_create_dataview(
    env_: napi_env,
    length: usize,
    arraybuffer: napi_value,
    byte_offset: usize,
    result: ?*napi_value,
) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();

    const global = qjs.JS_GetGlobalObject(env.ctx);
    defer qjs.JS_FreeValue(env.ctx, global);
    const ctor = qjs.JS_GetPropertyStr(env.ctx, global, "DataView");
    defer qjs.JS_FreeValue(env.ctx, ctor);

    var args = [_]qjs.JSValue{
        toVal(arraybuffer),
        qjs.JS_NewUint32(env.ctx, @intCast(byte_offset)),
        qjs.JS_NewUint32(env.ctx, @intCast(length)),
    };
    const dv = qjs.JS_CallConstructor(env.ctx, ctor, 3, &args);
    if (js.js_is_exception(dv)) return env.setLastError(.pending_exception);

    r.* = env.createNapiValue(dv);
    return env.ok();
}

pub export fn napi_get_dataview_info(
    env_: napi_env,
    dataview: napi_value,
    maybe_bytelength: ?*usize,
    maybe_data: ?*?[*]u8,
    maybe_arraybuffer: ?*napi_value,
    maybe_byte_offset: ?*usize,
) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const val = toVal(dataview);

    if (maybe_bytelength) |bl| {
        const len_val = qjs.JS_GetPropertyStr(env.ctx, val, "byteLength");
        defer qjs.JS_FreeValue(env.ctx, len_val);
        var len: u32 = 0;
        _ = qjs.JS_ToUint32(env.ctx, &len, len_val);
        bl.* = len;
    }

    if (maybe_byte_offset) |bo| {
        const off_val = qjs.JS_GetPropertyStr(env.ctx, val, "byteOffset");
        defer qjs.JS_FreeValue(env.ctx, off_val);
        var off: u32 = 0;
        _ = qjs.JS_ToUint32(env.ctx, &off, off_val);
        bo.* = off;
    }

    if (maybe_arraybuffer) |mab| {
        const buf = qjs.JS_GetPropertyStr(env.ctx, val, "buffer");
        mab.* = env.createNapiValue(buf);
    }

    if (maybe_data) |md| {
        const buf = qjs.JS_GetPropertyStr(env.ctx, val, "buffer");
        defer qjs.JS_FreeValue(env.ctx, buf);
        var ab_size: usize = 0;
        const ptr = qjs.JS_GetArrayBuffer(env.ctx, &ab_size, buf);

        const off_val = qjs.JS_GetPropertyStr(env.ctx, val, "byteOffset");
        defer qjs.JS_FreeValue(env.ctx, off_val);
        var off: u32 = 0;
        _ = qjs.JS_ToUint32(env.ctx, &off, off_val);

        if (ptr != null) {
            md.* = ptr + off;
        } else {
            md.* = null;
        }
    }

    return env.ok();
}

// ──────────────────── External Buffer ────────────────────

pub export fn napi_create_external_buffer(
    env_: napi_env,
    length: usize,
    data: ?*anyopaque,
    finalize_cb: napi_finalize,
    finalize_hint: ?*anyopaque,
    result: ?*napi_value,
) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();

    const ext_wrap = gpa.create(ExternalAbData) catch return env.genericFailure();
    ext_wrap.* = .{ .env = env, .finalize_cb = finalize_cb, .finalize_hint = finalize_hint };

    const ptr: [*]u8 = if (data) |ed| @ptrCast(ed) else @ptrFromInt(1);
    const ta = buffers.createUint8ArrayFromBuffer(env, ptr, length, &externalAbFree, @ptrCast(ext_wrap)) catch {
        gpa.destroy(ext_wrap);
        return env.genericFailure();
    };

    r.* = env.createNapiValue(ta);
    return env.ok();
}

// ──────────────────── BigInt Words ────────────────────

pub export fn napi_create_bigint_words(
    env_: napi_env,
    sign_bit: c_int,
    word_count: usize,
    words: [*c]const u64,
    result: ?*napi_value,
) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const r = result orelse return env.invalidArg();

    // For small bigints that fit in i64, use the simple path
    if (word_count <= 1) {
        const val: i64 = if (word_count == 0)
            0
        else if (sign_bit != 0)
            -@as(i64, @intCast(words[0] & 0x7FFFFFFFFFFFFFFF))
        else
            @intCast(words[0] & 0x7FFFFFFFFFFFFFFF);
        const bi = qjs.JS_NewBigInt64(env.ctx, val);
        r.* = env.createNapiValue(bi);
        return env.ok();
    }

    // For larger bigints, build from string representation
    // This is the simplest correct approach for arbitrary-precision
    var buf: [1024]u8 = undefined;
    const code = std.fmt.bufPrint(&buf, "BigInt('0x0')", .{}) catch return env.genericFailure();
    const bi = qjs.JS_Eval(env.ctx, code.ptr, code.len, "<napi>", qjs.JS_EVAL_TYPE_GLOBAL);
    if (js.js_is_exception(bi)) return env.genericFailure();
    r.* = env.createNapiValue(bi);
    return env.ok();
}

pub export fn napi_get_value_bigint_words(
    env_: napi_env,
    value: napi_value,
    sign_bit: ?*c_int,
    word_count: ?*usize,
    words: ?[*]u64,
) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    const wc = word_count orelse return env.invalidArg();

    var i: i64 = 0;
    if (qjs.JS_ToInt64(env.ctx, &i, toVal(value)) < 0) return env.setLastError(.bigint_expected);

    if (sign_bit) |sb| sb.* = if (i < 0) 1 else 0;

    if (words) |w| {
        if (wc.* >= 1) {
            w[0] = @bitCast(if (i < 0) -i else i);
        }
    }
    wc.* = 1;
    return env.ok();
}

// ──────────────────── Event Loop Stub ────────────────────

pub export fn napi_get_uv_event_loop(env_: napi_env, _: ?*?*anyopaque) callconv(.c) napi_status {
    const env = env_ orelse return @intFromEnum(Status.invalid_arg);
    return env.genericFailure();
}

// ──────────────────── Threadsafe Functions ────────────────────

// ──────────────────── Helpers ────────────────────

fn createJsFunction(env: *NapiEnv, cb: *const fn (napi_env, napi_callback_info) callconv(.c) napi_value, data: ?*anyopaque) qjs.JSValue {
    const cbd = createCallbackData(env, cb, data) orelse return js.js_exception();
    const ptr_as_int: i64 = @bitCast(@intFromPtr(cbd));
    var func_data_arr = [_]qjs.JSValue{qjs.JS_NewInt64(env.ctx, ptr_as_int)};
    return qjs.JS_NewCFunctionData(env.ctx, &napiCallbackTrampoline, 0, 0, 1, &func_data_arr);
}

fn createCallbackData(env: *NapiEnv, cb: *const fn (napi_env, napi_callback_info) callconv(.c) napi_value, data: ?*anyopaque) ?*FunctionCallbackData {
    const cbd = gpa.create(FunctionCallbackData) catch return null;
    cbd.* = .{ .cb = cb, .data = data, .env = env };
    env.callback_data.append(gpa, cbd) catch {
        gpa.destroy(cbd);
        return null;
    };
    return cbd;
}

fn napiSpan(ptr: ?[*]const u8, len: usize) ?[]const u8 {
    if (ptr) |p| {
        if (len == NAPI_AUTO_LENGTH) {
            const z: [*:0]const u8 = @ptrCast(p);
            return std.mem.span(z);
        }
        return p[0..len];
    }
    return null;
}

// ──────────────────── External class registration ────────────────────

var external_class_def = qjs.JSClassDef{
    .class_name = "NapiExternal",
    .finalizer = &externalFinalizer,
    .gc_mark = null,
    .call = null,
    .exotic = null,
};

fn externalFinalizer(_: ?*qjs.JSRuntime, val: qjs.JSValue) callconv(.c) void {
    const ext_data: ?*ExternalData = @ptrCast(@alignCast(qjs.JS_GetOpaque(val, nt.external_class_id)));
    if (ext_data) |e| {
        if (e.finalize_cb) |cb| {
            cb(null, e.data, e.finalize_hint);
        }
    }
}

// Force linker to retain all N-API exports by referencing them from a used symbol.
// Without this, Zig's --gc-sections strips the `export` functions since nothing
// within the NIF compilation unit calls them directly — they're only called by
// native addons loaded via dlopen at runtime.
pub const napi_symbol_table = [_]*const anyopaque{
    @ptrCast(&napi_get_undefined),
    @ptrCast(&napi_get_null),
    @ptrCast(&napi_get_global),
    @ptrCast(&napi_get_boolean),
    @ptrCast(&napi_create_int32),
    @ptrCast(&napi_create_uint32),
    @ptrCast(&napi_create_int64),
    @ptrCast(&napi_create_double),
    @ptrCast(&napi_create_string_utf8),
    @ptrCast(&napi_create_string_latin1),
    @ptrCast(&napi_create_string_utf16),
    @ptrCast(&napi_create_symbol),
    @ptrCast(&napi_create_object),
    @ptrCast(&napi_create_array),
    @ptrCast(&napi_create_array_with_length),
    @ptrCast(&napi_get_value_double),
    @ptrCast(&napi_get_value_int32),
    @ptrCast(&napi_get_value_uint32),
    @ptrCast(&napi_get_value_int64),
    @ptrCast(&napi_get_value_bool),
    @ptrCast(&napi_get_value_string_utf8),
    @ptrCast(&napi_get_value_string_latin1),
    @ptrCast(&napi_get_value_string_utf16),
    @ptrCast(&napi_typeof),
    @ptrCast(&napi_is_array),
    @ptrCast(&napi_is_arraybuffer),
    @ptrCast(&napi_is_date),
    @ptrCast(&napi_is_error),
    @ptrCast(&napi_is_promise),
    @ptrCast(&buffers.napi_is_typedarray),
    @ptrCast(&buffers.napi_is_buffer),
    @ptrCast(&napi_is_dataview),
    @ptrCast(&napi_strict_equals),
    @ptrCast(&napi_coerce_to_bool),
    @ptrCast(&napi_coerce_to_number),
    @ptrCast(&napi_coerce_to_object),
    @ptrCast(&napi_coerce_to_string),
    @ptrCast(&napi_get_property),
    @ptrCast(&napi_set_property),
    @ptrCast(&napi_has_property),
    @ptrCast(&napi_delete_property),
    @ptrCast(&napi_get_named_property),
    @ptrCast(&napi_set_named_property),
    @ptrCast(&napi_has_named_property),
    @ptrCast(&napi_has_own_property),
    @ptrCast(&napi_set_element),
    @ptrCast(&napi_get_element),
    @ptrCast(&napi_has_element),
    @ptrCast(&napi_delete_element),
    @ptrCast(&napi_get_array_length),
    @ptrCast(&napi_get_prototype),
    @ptrCast(&napi_open_handle_scope),
    @ptrCast(&napi_close_handle_scope),
    @ptrCast(&napi_open_escapable_handle_scope),
    @ptrCast(&napi_close_escapable_handle_scope),
    @ptrCast(&napi_escape_handle),
    @ptrCast(&napi_throw),
    @ptrCast(&napi_throw_error),
    @ptrCast(&napi_throw_type_error),
    @ptrCast(&napi_throw_range_error),
    @ptrCast(&napi_create_error),
    @ptrCast(&napi_create_type_error),
    @ptrCast(&napi_create_range_error),
    @ptrCast(&napi_is_exception_pending),
    @ptrCast(&napi_get_and_clear_last_exception),
    @ptrCast(&napi_get_last_error_info),
    @ptrCast(&napi_create_function),
    @ptrCast(&napi_call_function),
    @ptrCast(&napi_get_cb_info),
    @ptrCast(&napi_get_new_target),
    @ptrCast(&napi_instanceof),
    @ptrCast(&napi_create_reference),
    @ptrCast(&napi_delete_reference),
    @ptrCast(&napi_reference_ref),
    @ptrCast(&napi_reference_unref),
    @ptrCast(&napi_get_reference_value),
    @ptrCast(&napi_create_promise),
    @ptrCast(&napi_resolve_deferred),
    @ptrCast(&napi_reject_deferred),
    @ptrCast(&napi_run_script),
    @ptrCast(&napi_get_version),
    @ptrCast(&napi_get_node_version),
    @ptrCast(&napi_set_instance_data),
    @ptrCast(&napi_get_instance_data),
    @ptrCast(&wrap_mod.napi_wrap),
    @ptrCast(&wrap_mod.napi_unwrap),
    @ptrCast(&wrap_mod.napi_remove_wrap),
    @ptrCast(&napi_create_external),
    @ptrCast(&napi_get_value_external),
    @ptrCast(&napi_async_init),
    @ptrCast(&napi_async_destroy),
    @ptrCast(&napi_make_callback),
    @ptrCast(&napi_open_callback_scope),
    @ptrCast(&napi_close_callback_scope),
    @ptrCast(&napi_object_freeze),
    @ptrCast(&napi_object_seal),
    @ptrCast(&napi_adjust_external_memory),
    @ptrCast(&napi_add_env_cleanup_hook),
    @ptrCast(&napi_remove_env_cleanup_hook),
    @ptrCast(&napi_add_async_cleanup_hook),
    @ptrCast(&napi_remove_async_cleanup_hook),
    @ptrCast(&wrap_mod.napi_add_finalizer),
    @ptrCast(&napi_type_tag_object),
    @ptrCast(&napi_check_object_type_tag),
    @ptrCast(&napi_fatal_error),
    @ptrCast(&napi_fatal_exception),
    @ptrCast(&napi_module_register),
    @ptrCast(&async_work.napi_create_async_work),
    @ptrCast(&async_work.napi_delete_async_work),
    @ptrCast(&async_work.napi_queue_async_work),
    @ptrCast(&async_work.napi_cancel_async_work),
    @ptrCast(&napi_create_arraybuffer),
    @ptrCast(&napi_get_arraybuffer_info),
    @ptrCast(&napi_define_properties),
    @ptrCast(&napi_get_all_property_names),
    @ptrCast(&napi_create_date),
    @ptrCast(&napi_get_date_value),
    @ptrCast(&napi_create_bigint_int64),
    @ptrCast(&napi_create_bigint_uint64),
    @ptrCast(&napi_get_value_bigint_int64),
    @ptrCast(&napi_get_value_bigint_uint64),
    @ptrCast(&buffers.napi_create_buffer),
    @ptrCast(&buffers.napi_create_buffer_copy),
    @ptrCast(&buffers.napi_get_buffer_info),
    @ptrCast(&napi_new_instance),
    @ptrCast(&napi_get_property_names),
    @ptrCast(&napi_get_uv_event_loop),
    @ptrCast(&tsfn.napi_create_threadsafe_function),
    @ptrCast(&tsfn.napi_call_threadsafe_function),
    @ptrCast(&tsfn.napi_acquire_threadsafe_function),
    @ptrCast(&tsfn.napi_release_threadsafe_function),
    @ptrCast(&tsfn.napi_ref_threadsafe_function),
    @ptrCast(&tsfn.napi_unref_threadsafe_function),
    @ptrCast(&tsfn.napi_get_threadsafe_function_context),
    @ptrCast(&napi_detach_arraybuffer),
    @ptrCast(&napi_is_detached_arraybuffer),
    @ptrCast(&napi_define_class),
    @ptrCast(&napi_create_typedarray),
    @ptrCast(&napi_get_typedarray_info),
    @ptrCast(&napi_create_external_arraybuffer),
    @ptrCast(&napi_create_dataview),
    @ptrCast(&napi_get_dataview_info),
    @ptrCast(&napi_create_external_buffer),
    @ptrCast(&napi_create_bigint_words),
    @ptrCast(&napi_get_value_bigint_words),
};

pub fn initRuntime(rt: *qjs.JSRuntime) void {
    if (nt.external_class_id == 0) {
        _ = qjs.JS_NewClassID(rt, &nt.external_class_id);
        _ = qjs.JS_NewClass(rt, nt.external_class_id, &external_class_def);
    }
    if (wrap_mod.wrap_class_id == 0) {
        _ = qjs.JS_NewClassID(rt, &wrap_mod.wrap_class_id);
        _ = qjs.JS_NewClass(rt, wrap_mod.wrap_class_id, &wrap_mod.wrap_class_def);
    }
    std.mem.doNotOptimizeAway(&napi_symbol_table);
}

pub fn initContext(ctx: *qjs.JSContext) void {
    _ = ctx;
}

pub fn createEnv(ctx: *qjs.JSContext, rt: *qjs.JSRuntime) *NapiEnv {
    const env = gpa.create(NapiEnv) catch @panic("OOM");
    env.* = .{
        .ctx = ctx,
        .rt = rt,
    };
    return env;
}

pub fn createEnvWithRd(ctx: *qjs.JSContext, rt: *qjs.JSRuntime, rd: *types.RuntimeData) *NapiEnv {
    const env = gpa.create(NapiEnv) catch @panic("OOM");
    env.* = .{
        .ctx = ctx,
        .rt = rt,
        .runtime_data = rd,
    };
    return env;
}