usingnamespace @import("../base.zig");
const std = @import("std");
const Api = @import("../../../api/schema.zig").Api;
const http = @import("../../../http.zig");
usingnamespace @import("../javascript.zig");
usingnamespace @import("../bindings/bindings.zig");
const ZigURL = @import("../../../query_string_map.zig").URL;
const HTTPClient = @import("../../../http_client.zig");
const Method = @import("../../../http/method.zig").Method;

const picohttp = @import("picohttp");
pub const Response = struct {
    pub const Class = NewClass(
        Response,
        .{ .name = "Response" },
        .{
            .@"constructor" = constructor,
            .@"text" = .{
                .rfn = getText,
                .ts = d.ts{},
            },
            .@"json" = .{
                .rfn = getJson,
                .ts = d.ts{},
            },
            .@"arrayBuffer" = .{
                .rfn = getArrayBuffer,
                .ts = d.ts{},
            },
        },
        .{
            // .@"url" = .{
            //     .@"get" = getURL,
            //     .ro = true,
            // },

            .@"ok" = .{
                .@"get" = getOK,
                .ro = true,
            },
            .@"status" = .{
                .@"get" = getStatus,
                .ro = true,
            },
        },
    );

    allocator: *std.mem.Allocator,
    body: Body,
    status_text: string = "",

    pub const Props = struct {};

    pub fn getOK(
        this: *Response,
        ctx: js.JSContextRef,
        thisObject: js.JSValueRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        // https://developer.mozilla.org/en-US/docs/Web/API/Response/ok
        return js.JSValueMakeBoolean(ctx, this.body.init.status_code == 304 or (this.body.init.status_code >= 200 and this.body.init.status_code <= 299));
    }

    pub fn getText(
        this: *Response,
        ctx: js.JSContextRef,
        function: js.JSObjectRef,
        thisObject: js.JSObjectRef,
        arguments: []const js.JSValueRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        // https://developer.mozilla.org/en-US/docs/Web/API/Response/text
        defer this.body.value = .Empty;
        return JSPromise.resolvedPromiseValue(
            VirtualMachine.vm.global,
            (brk: {
                switch (this.body.value) {
                    .Unconsumed => {
                        if (this.body.len > 0) {
                            if (this.body.ptr) |_ptr| {
                                var offset: usize = 0;
                                while (offset < this.body.len and (_ptr[offset] > 127 or strings.utf8ByteSequenceLength(_ptr[offset]) == 0)) : (offset += 1) {}
                                if (offset < this.body.len) {
                                    break :brk ZigString.init(_ptr[offset..this.body.len]).toValue(VirtualMachine.vm.global);
                                }
                            }
                        }

                        break :brk ZigString.init("").toValue(VirtualMachine.vm.global);
                    },
                    .Empty => {
                        break :brk ZigString.init("").toValue(VirtualMachine.vm.global);
                    },
                    .String => |str| {
                        break :brk ZigString.init(str).toValue(VirtualMachine.vm.global);
                    },
                    .ArrayBuffer => |buffer| {
                        break :brk ZigString.init(buffer.ptr[buffer.offset..buffer.byte_len]).toValue(VirtualMachine.vm.global);
                    },
                }
            }),
        ).asRef();
    }

    var temp_error_buffer: [4096]u8 = undefined;
    var error_arg_list: [1]js.JSObjectRef = undefined;
    pub fn getJson(
        this: *Response,
        ctx: js.JSContextRef,
        function: js.JSObjectRef,
        thisObject: js.JSObjectRef,
        arguments: []const js.JSValueRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        var zig_string = ZigString.init("");
        var deallocate = false;
        defer {
            if (deallocate) {
                if (this.body.value == .Unconsumed) {
                    this.body.ptr_allocator.?.free(this.body.ptr.?[0..this.body.len]);
                    this.body.ptr_allocator = null;
                    this.body.ptr = null;
                    this.body.len = 0;
                }
            }

            this.body.value = .Empty;
        }

        var json_value = (js.JSValueMakeFromJSONString(
            ctx,
            brk: {
                switch (this.body.value) {
                    .Unconsumed => {
                        if (this.body.ptr) |_ptr| {
                            zig_string = ZigString.init(_ptr[0..this.body.len]);
                            deallocate = true;

                            break :brk zig_string.toJSStringRef();
                        }

                        break :brk zig_string.toJSStringRef();
                    },
                    .Empty => {
                        break :brk zig_string.toJSStringRef();
                    },
                    .String => |str| {
                        zig_string = ZigString.init(str);
                        break :brk zig_string.toJSStringRef();
                    },
                    .ArrayBuffer => |buffer| {
                        zig_string = ZigString.init(buffer.ptr[buffer.offset..buffer.byte_len]);
                        break :brk zig_string.toJSStringRef();
                    },
                }
            },
        ) orelse {
            var out = std.fmt.bufPrint(&temp_error_buffer, "Invalid JSON\n\n \"{s}\"", .{zig_string.slice()[0..std.math.min(zig_string.len, 4000)]}) catch unreachable;
            error_arg_list[0] = ZigString.init(out).toValueGC(VirtualMachine.vm.global).asRef();
            return JSPromise.rejectedPromiseValue(
                VirtualMachine.vm.global,
                JSValue.fromRef(
                    js.JSObjectMakeError(
                        ctx,
                        1,
                        &error_arg_list,
                        exception,
                    ),
                ),
            ).asRef();
        });

        return JSPromise.resolvedPromiseValue(
            VirtualMachine.vm.global,
            JSValue.fromRef(json_value),
        ).asRef();
    }
    pub fn getArrayBuffer(
        this: *Response,
        ctx: js.JSContextRef,
        function: js.JSObjectRef,
        thisObject: js.JSObjectRef,
        arguments: []const js.JSValueRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        defer this.body.value = .Empty;
        return JSPromise.resolvedPromiseValue(
            VirtualMachine.vm.global,
            JSValue.fromRef(
                (brk: {
                    switch (this.body.value) {
                        .Unconsumed => {
                            if (this.body.ptr) |_ptr| {
                                break :brk js.JSObjectMakeTypedArrayWithBytesNoCopy(
                                    ctx,
                                    js.JSTypedArrayType.kJSTypedArrayTypeUint8Array,
                                    _ptr,
                                    this.body.len,
                                    null,
                                    null,
                                    exception,
                                );
                            }

                            break :brk js.JSObjectMakeTypedArray(
                                ctx,
                                js.JSTypedArrayType.kJSTypedArrayTypeUint8Array,
                                0,
                                exception,
                            );
                        },
                        .Empty => {
                            break :brk js.JSObjectMakeTypedArray(ctx, js.JSTypedArrayType.kJSTypedArrayTypeUint8Array, 0, exception);
                        },
                        .String => |str| {
                            break :brk js.JSObjectMakeTypedArrayWithBytesNoCopy(
                                ctx,
                                js.JSTypedArrayType.kJSTypedArrayTypeUint8Array,
                                @intToPtr([*]u8, @ptrToInt(str.ptr)),
                                str.len,
                                null,
                                null,
                                exception,
                            );
                        },
                        .ArrayBuffer => |buffer| {
                            break :brk js.JSObjectMakeTypedArrayWithBytesNoCopy(
                                ctx,
                                buffer.typed_array_type,
                                buffer.ptr,
                                buffer.byte_len,
                                null,
                                null,
                                exception,
                            );
                        },
                    }
                }),
            ),
        ).asRef();
    }

    pub fn getStatus(
        this: *Response,
        ctx: js.JSContextRef,
        thisObject: js.JSValueRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        // https://developer.mozilla.org/en-US/docs/Web/API/Response/status
        return js.JSValueMakeNumber(ctx, @intToFloat(f64, this.body.init.status_code));
    }

    pub fn finalize(
        this: *Response,
    ) void {
        this.body.deinit(this.allocator);
        this.allocator.destroy(this);
    }

    pub fn mimeType(response: *const Response, request_ctx: *const http.RequestContext) string {
        if (response.body.init.headers) |headers| {
            // Remember, we always lowercase it
            // hopefully doesn't matter here tho
            if (headers.getHeaderIndex("content-type")) |content_type| {
                return headers.asStr(headers.entries.items(.value)[content_type]);
            }
        }

        if (request_ctx.url.extname.len > 0) {
            return http.MimeType.byExtension(request_ctx.url.extname).value;
        }

        switch (response.body.value) {
            .Empty => {
                return "text/plain";
            },
            .String => |body| {
                // poor man's mimetype sniffing
                if (body.len > 0 and (body[0] == '{' or body[0] == '[')) {
                    return http.MimeType.json.value;
                }

                return http.MimeType.html.value;
            },
            .Unconsumed, .ArrayBuffer => {
                return "application/octet-stream";
            },
        }
    }

    pub fn constructor(
        ctx: js.JSContextRef,
        function: js.JSObjectRef,
        arguments: []const js.JSValueRef,
        exception: js.ExceptionRef,
    ) js.JSObjectRef {
        const body: Body = brk: {
            switch (arguments.len) {
                0 => {
                    break :brk Body.@"404"(ctx);
                },
                1 => {
                    break :brk Body.extract(ctx, arguments[0], exception);
                },
                else => {
                    if (js.JSValueGetType(ctx, arguments[1]) == js.JSType.kJSTypeObject) {
                        break :brk Body.extractWithInit(ctx, arguments[0], arguments[1], exception);
                    } else {
                        break :brk Body.extract(ctx, arguments[0], exception);
                    }
                },
            }
            unreachable;
        };

        // if (exception != null) {
        //     return null;
        // }

        var response = getAllocator(ctx).create(Response) catch unreachable;
        response.* = Response{
            .body = body,
            .allocator = getAllocator(ctx),
        };
        return Response.Class.make(
            ctx,
            response,
        );
    }
};

pub const Fetch = struct {
    const headers_string = "headers";
    const method_string = "method";

    var fetch_body_string: MutableString = undefined;
    var fetch_body_string_loaded = false;

    const JSType = js.JSType;

    const fetch_error_no_args = "fetch() expects a string but received no arguments.";
    const fetch_error_blank_url = "fetch() URL must not be a blank string.";
    const JSTypeErrorEnum = std.enums.EnumArray(JSType, string);
    const fetch_type_error_names: JSTypeErrorEnum = brk: {
        var errors = JSTypeErrorEnum.initUndefined();
        errors.set(JSType.kJSTypeUndefined, "Undefined");
        errors.set(JSType.kJSTypeNull, "Null");
        errors.set(JSType.kJSTypeBoolean, "Boolean");
        errors.set(JSType.kJSTypeNumber, "Number");
        errors.set(JSType.kJSTypeString, "String");
        errors.set(JSType.kJSTypeObject, "Object");
        errors.set(JSType.kJSTypeSymbol, "Symbol");
        break :brk errors;
    };

    const fetch_type_error_string_values = .{
        std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeUndefined)}),
        std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeNull)}),
        std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeBoolean)}),
        std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeNumber)}),
        std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeString)}),
        std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeObject)}),
        std.fmt.comptimePrint("fetch() expects a string, but received {s}", .{fetch_type_error_names.get(JSType.kJSTypeSymbol)}),
    };

    const fetch_type_error_strings: JSTypeErrorEnum = brk: {
        var errors = JSTypeErrorEnum.initUndefined();
        errors.set(
            JSType.kJSTypeUndefined,
            std.mem.span(fetch_type_error_string_values[0]),
        );
        errors.set(
            JSType.kJSTypeNull,
            std.mem.span(fetch_type_error_string_values[1]),
        );
        errors.set(
            JSType.kJSTypeBoolean,
            std.mem.span(fetch_type_error_string_values[2]),
        );
        errors.set(
            JSType.kJSTypeNumber,
            std.mem.span(fetch_type_error_string_values[3]),
        );
        errors.set(
            JSType.kJSTypeString,
            std.mem.span(fetch_type_error_string_values[4]),
        );
        errors.set(
            JSType.kJSTypeObject,
            std.mem.span(fetch_type_error_string_values[5]),
        );
        errors.set(
            JSType.kJSTypeSymbol,
            std.mem.span(fetch_type_error_string_values[6]),
        );
        break :brk errors;
    };

    pub const Class = NewClass(
        void,
        .{ .name = "fetch" },
        .{
            .@"call" = .{
                .rfn = Fetch.call,
                .ts = d.ts{},
            },
        },
        .{},
    );

    const fetch_error_cant_fetch_same_origin = "fetch to same-origin on the server is not supported yet - sorry! (it would just hang forever)";
    pub fn call(
        this: void,
        ctx: js.JSContextRef,
        function: js.JSObjectRef,
        thisObject: js.JSObjectRef,
        arguments: []const js.JSValueRef,
        exception: js.ExceptionRef,
    ) js.JSObjectRef {
        if (arguments.len == 0) {
            const fetch_error = fetch_error_no_args;
            return JSPromise.rejectedPromiseValue(VirtualMachine.vm.global, ZigString.init(fetch_error).toErrorInstance(VirtualMachine.vm.global)).asRef();
        }

        if (!js.JSValueIsString(ctx, arguments[0])) {
            const fetch_error = fetch_type_error_strings.get(js.JSValueGetType(ctx, arguments[0]));
            return JSPromise.rejectedPromiseValue(VirtualMachine.vm.global, ZigString.init(fetch_error).toErrorInstance(VirtualMachine.vm.global)).asRef();
        }

        var url_zig_str = ZigString.init("");
        JSValue.fromRef(arguments[0]).toZigString(
            &url_zig_str,
            VirtualMachine.vm.global,
        );
        var url_str = url_zig_str.slice();
        if (url_str.len == 0) {
            const fetch_error = fetch_error_blank_url;
            return JSPromise.rejectedPromiseValue(VirtualMachine.vm.global, ZigString.init(fetch_error).toErrorInstance(VirtualMachine.vm.global)).asRef();
        }

        var dealloc_url_str = false;
        if (url_str[0] == '/') {
            url_str = strings.append(getAllocator(ctx), VirtualMachine.vm.bundler.options.origin.origin, url_str) catch unreachable;
            dealloc_url_str = true;
        }
        defer if (dealloc_url_str) getAllocator(ctx).free(url_str);

        var http_client = HTTPClient.init(getAllocator(ctx), .GET, ZigURL.parse(url_str), .{}, "");

        if (http_client.url.origin.len > 0 and strings.eql(http_client.url.origin, VirtualMachine.vm.bundler.options.origin.origin)) {
            const fetch_error = fetch_error_cant_fetch_same_origin;
            return JSPromise.rejectedPromiseValue(VirtualMachine.vm.global, ZigString.init(fetch_error).toErrorInstance(VirtualMachine.vm.global)).asRef();
        }

        var headers: ?Headers = null;
        var body: string = "";

        if (arguments.len >= 2 and js.JSValueIsObject(ctx, arguments[1])) {
            var array = js.JSObjectCopyPropertyNames(ctx, arguments[1]);
            defer js.JSPropertyNameArrayRelease(array);
            const count = js.JSPropertyNameArrayGetCount(array);
            var i: usize = 0;
            while (i < count) : (i += 1) {
                var property_name_ref = js.JSPropertyNameArrayGetNameAtIndex(array, i);
                switch (js.JSStringGetLength(property_name_ref)) {
                    "headers".len => {
                        if (js.JSStringIsEqualToUTF8CString(property_name_ref, "headers")) {
                            if (js.JSObjectGetProperty(ctx, arguments[1], property_name_ref, null)) |value| {
                                if (GetJSPrivateData(Headers, value)) |headers_ptr| {
                                    headers = headers_ptr.*;
                                } else if (Headers.JS.headersInit(ctx, value) catch null) |headers_| {
                                    headers = headers_;
                                }
                            }
                        }
                    },
                    "body".len => {
                        if (js.JSStringIsEqualToUTF8CString(property_name_ref, "body")) {
                            if (js.JSObjectGetProperty(ctx, arguments[1], property_name_ref, null)) |value| {
                                var body_ = Body.extractBody(ctx, value, false, null, exception);
                                if (exception.* != null) return js.JSValueMakeNull(ctx);
                                switch (body_.value) {
                                    .ArrayBuffer => |arraybuffer| {
                                        body = arraybuffer.ptr[0..arraybuffer.byte_len];
                                    },
                                    .String => |str| {
                                        body = str;
                                    },
                                    else => {},
                                }
                            }
                        }
                    },
                    "method".len => {
                        if (js.JSStringIsEqualToUTF8CString(property_name_ref, "method")) {
                            if (js.JSObjectGetProperty(ctx, arguments[1], property_name_ref, null)) |value| {
                                var string_ref = js.JSValueToStringCopy(ctx, value, exception);

                                if (exception.* != null) return js.JSValueMakeNull(ctx);
                                defer js.JSStringRelease(string_ref);
                                var method_name_buf: [16]u8 = undefined;
                                var method_name = method_name_buf[0..js.JSStringGetUTF8CString(string_ref, &method_name_buf, method_name_buf.len)];
                                http_client.method = Method.which(method_name) orelse http_client.method;
                            }
                        }
                    },
                    else => {},
                }
            }
        }

        if (headers) |head| {
            http_client.header_entries = head.entries;
            http_client.header_buf = head.buf.items;
        }

        if (fetch_body_string_loaded) {
            fetch_body_string.reset();
        } else {
            fetch_body_string = MutableString.init(VirtualMachine.vm.allocator, 0) catch unreachable;
            fetch_body_string_loaded = true;
        }

        var http_response = http_client.send(body, &fetch_body_string) catch |err| {
            const fetch_error = std.fmt.allocPrint(
                getAllocator(ctx),
                "Fetch error: {s}\nURL: \"{s}\"",
                .{
                    @errorName(err),
                    url_str,
                },
            ) catch unreachable;
            return JSPromise.rejectedPromiseValue(VirtualMachine.vm.global, ZigString.init(fetch_error).toErrorInstance(VirtualMachine.vm.global)).asRef();
        };

        var response_headers = Headers.fromPicoHeaders(getAllocator(ctx), http_response.headers) catch unreachable;
        response_headers.guard = .immutable;
        var response = getAllocator(ctx).create(Response) catch unreachable;
        var allocator = getAllocator(ctx);
        var duped = allocator.dupeZ(u8, fetch_body_string.list.items) catch unreachable;
        response.* = Response{
            .allocator = allocator,
            .status_text = allocator.dupe(u8, http_response.status) catch unreachable,
            .body = .{
                .init = .{
                    .headers = response_headers,
                    .status_code = @truncate(u16, http_response.status_code),
                },
                .value = .{
                    .Unconsumed = 0,
                },
                .ptr = duped.ptr,
                .len = duped.len,
                .ptr_allocator = allocator,
            },
        };

        return JSPromise.resolvedPromiseValue(
            VirtualMachine.vm.global,
            JSValue.fromRef(Response.Class.make(ctx, response)),
        ).asRef();
    }
};

// https://developer.mozilla.org/en-US/docs/Web/API/Headers
pub const Headers = struct {
    pub const Kv = struct {
        name: Api.StringPointer,
        value: Api.StringPointer,
    };
    pub const Entries = std.MultiArrayList(Kv);
    entries: Entries,
    buf: std.ArrayListUnmanaged(u8),
    allocator: *std.mem.Allocator,
    used: u32 = 0,
    guard: Guard = Guard.none,

    pub fn deinit(
        headers: *Headers,
    ) void {
        headers.buf.deinit(headers.allocator);
        headers.entries.deinit(headers.allocator);
    }

    // https://developer.mozilla.org/en-US/docs/Web/API/Headers#methods
    pub const JS = struct {

        // https://developer.mozilla.org/en-US/docs/Web/API/Headers/get
        pub fn get(
            this: *Headers,
            ctx: js.JSContextRef,
            function: js.JSObjectRef,
            thisObject: js.JSObjectRef,
            arguments: []const js.JSValueRef,
            exception: js.ExceptionRef,
        ) js.JSValueRef {
            if (arguments.len == 0 or !js.JSValueIsString(ctx, arguments[0]) or js.JSStringIsEqual(arguments[0], Properties.Refs.empty_string)) {
                return js.JSValueMakeNull(ctx);
            }

            const key_len = js.JSStringGetUTF8CString(arguments[0], &header_kv_buf, header_kv_buf.len);
            const key = header_kv_buf[0 .. key_len - 1];
            if (this.getHeaderIndex(key)) |index| {
                var str = this.asStr(this.entries.items(.value)[index]);
                var ref = js.JSStringCreateWithUTF8CString(str.ptr);
                defer js.JSStringRelease(ref);
                return js.JSValueMakeString(ctx, ref);
            } else {
                return js.JSValueMakeNull(ctx);
            }
        }

        // https://developer.mozilla.org/en-US/docs/Web/API/Headers/set
        // > The difference between set() and Headers.append is that if the specified header already exists and accepts multiple values
        // > set() overwrites the existing value with the new one, whereas Headers.append appends the new value to the end of the set of values.
        pub fn set(
            this: *Headers,
            ctx: js.JSContextRef,
            function: js.JSObjectRef,
            thisObject: js.JSObjectRef,
            arguments: []const js.JSValueRef,
            exception: js.ExceptionRef,
        ) js.JSValueRef {
            if (this.guard == .request or arguments.len < 2 or !js.JSValueIsString(ctx, arguments[0]) or js.JSStringIsEqual(arguments[0], Properties.Refs.empty_string) or !js.JSValueIsString(ctx, arguments[1])) {
                return js.JSValueMakeUndefined(ctx);
            }

            this.putHeader(arguments[0], arguments[1], false);
            return js.JSValueMakeUndefined(ctx);
        }

        // https://developer.mozilla.org/en-US/docs/Web/API/Headers/append
        pub fn append(
            this: *Headers,
            ctx: js.JSContextRef,
            function: js.JSObjectRef,
            thisObject: js.JSObjectRef,
            arguments: []const js.JSValueRef,
            exception: js.ExceptionRef,
        ) js.JSValueRef {
            if (this.guard == .request or arguments.len < 2 or !js.JSValueIsString(ctx, arguments[0]) or js.JSStringIsEqual(arguments[0], Properties.Refs.empty_string) or !js.JSValueIsString(ctx, arguments[1])) {
                return js.JSValueMakeUndefined(ctx);
            }

            this.putHeader(arguments[0], arguments[1], true);
            return js.JSValueMakeUndefined(ctx);
        }
        pub fn delete(
            this: *Headers,
            ctx: js.JSContextRef,
            function: js.JSObjectRef,
            thisObject: js.JSObjectRef,
            arguments: []const js.JSValueRef,
            exception: js.ExceptionRef,
        ) js.JSValueRef {
            if (this.guard == .request or arguments.len < 1 or !js.JSValueIsString(ctx, arguments[0]) or js.JSStringIsEqual(arguments[0], Properties.Refs.empty_string)) {
                return js.JSValueMakeUndefined(ctx);
            }

            const key_len = js.JSStringGetUTF8CString(arguments[0], &header_kv_buf, header_kv_buf.len) - 1;
            const key = header_kv_buf[0..key_len];

            if (this.getHeaderIndex(key)) |header_i| {
                this.entries.orderedRemove(header_i);
            }

            return js.JSValueMakeUndefined(ctx);
        }
        pub fn entries(
            this: *Headers,
            ctx: js.JSContextRef,
            function: js.JSObjectRef,
            thisObject: js.JSObjectRef,
            arguments: []const js.JSValueRef,
            exception: js.ExceptionRef,
        ) js.JSValueRef {
            Output.prettyErrorln("<r><b>Headers.entries()<r> is not implemented yet - sorry!!", .{});
            return js.JSValueMakeNull(ctx);
        }
        pub fn keys(
            this: *Headers,
            ctx: js.JSContextRef,
            function: js.JSObjectRef,
            thisObject: js.JSObjectRef,
            arguments: []const js.JSValueRef,
            exception: js.ExceptionRef,
        ) js.JSValueRef {
            Output.prettyErrorln("H<r><b>eaders.keys()<r> is not implemented yet- sorry!!", .{});
            return js.JSValueMakeNull(ctx);
        }
        pub fn values(
            this: *Headers,
            ctx: js.JSContextRef,
            function: js.JSObjectRef,
            thisObject: js.JSObjectRef,
            arguments: []const js.JSValueRef,
            exception: js.ExceptionRef,
        ) js.JSValueRef {
            Output.prettyErrorln("<r><b>Headers.values()<r> is not implemented yet - sorry!!", .{});
            return js.JSValueMakeNull(ctx);
        }

        pub fn headersInit(ctx: js.JSContextRef, header_prop: js.JSObjectRef) !?Headers {
            const header_keys = js.JSObjectCopyPropertyNames(ctx, header_prop);
            defer js.JSPropertyNameArrayRelease(header_keys);
            const total_header_count = js.JSPropertyNameArrayGetCount(header_keys);
            if (total_header_count == 0) return null;

            // 2 passes through the headers

            // Pass #1: find the "real" count.
            // The number of things which are strings or numbers.
            // Anything else should be ignored.
            // We could throw a TypeError, but ignoring silently is more JavaScript-like imo
            var real_header_count: usize = 0;
            var estimated_buffer_len: usize = 0;
            var j: usize = 0;
            while (j < total_header_count) : (j += 1) {
                var key_ref = js.JSPropertyNameArrayGetNameAtIndex(header_keys, j);
                var value_ref = js.JSObjectGetProperty(ctx, header_prop, key_ref, null);

                switch (js.JSValueGetType(ctx, value_ref)) {
                    js.JSType.kJSTypeNumber => {
                        const key_len = js.JSStringGetLength(key_ref);
                        if (key_len > 0) {
                            real_header_count += 1;
                            estimated_buffer_len += key_len;
                            estimated_buffer_len += std.fmt.count("{d}", .{js.JSValueToNumber(ctx, value_ref, null)});
                        }
                    },
                    js.JSType.kJSTypeString => {
                        const key_len = js.JSStringGetLength(key_ref);
                        const value_len = js.JSStringGetLength(value_ref);
                        if (key_len > 0 and value_len > 0) {
                            real_header_count += 1;
                            estimated_buffer_len += key_len + value_len;
                        }
                    },
                    else => {},
                }
            }

            if (real_header_count == 0 or estimated_buffer_len == 0) return null;

            j = 0;
            var allocator = getAllocator(ctx);
            var headers = Headers{
                .allocator = allocator,
                .buf = try std.ArrayListUnmanaged(u8).initCapacity(allocator, estimated_buffer_len),
                .entries = Headers.Entries{},
            };
            errdefer headers.deinit();
            try headers.entries.ensureTotalCapacity(allocator, real_header_count);
            headers.buf.expandToCapacity();
            while (j < total_header_count) : (j += 1) {
                var key_ref = js.JSPropertyNameArrayGetNameAtIndex(header_keys, j);
                var value_ref = js.JSObjectGetProperty(ctx, header_prop, key_ref, null);

                switch (js.JSValueGetType(ctx, value_ref)) {
                    js.JSType.kJSTypeNumber => {
                        if (js.JSStringGetLength(key_ref) == 0) continue;
                        try headers.appendInit(ctx, key_ref, .kJSTypeNumber, value_ref);
                    },
                    js.JSType.kJSTypeString => {
                        if (js.JSStringGetLength(value_ref) == 0 or js.JSStringGetLength(key_ref) == 0) continue;
                        try headers.appendInit(ctx, key_ref, .kJSTypeString, value_ref);
                    },
                    else => {},
                }
            }
            return headers;
        }

        // https://developer.mozilla.org/en-US/docs/Web/API/Headers/Headers
        pub fn constructor(
            ctx: js.JSContextRef,
            function: js.JSObjectRef,
            arguments: []const js.JSValueRef,
            exception: js.ExceptionRef,
        ) js.JSObjectRef {
            var headers = getAllocator(ctx).create(Headers) catch unreachable;
            if (arguments.len > 0 and js.JSValueIsObjectOfClass(ctx, arguments[0], Headers.Class.get().*)) {
                var other = castObj(arguments[0], Headers);
                other.clone(headers) catch unreachable;
            } else if (arguments.len == 1 and js.JSValueIsObject(ctx, arguments[0])) {
                headers.* = (JS.headersInit(ctx, arguments[0]) catch unreachable) orelse Headers{
                    .entries = @TypeOf(headers.entries){},
                    .buf = @TypeOf(headers.buf){},
                    .used = 0,
                    .allocator = getAllocator(ctx),
                    .guard = Guard.none,
                };
            } else {
                headers.* = Headers{
                    .entries = @TypeOf(headers.entries){},
                    .buf = @TypeOf(headers.buf){},
                    .used = 0,
                    .allocator = getAllocator(ctx),
                    .guard = Guard.none,
                };
            }

            return Headers.Class.make(ctx, headers);
        }

        pub fn finalize(
            this: *Headers,
        ) void {
            this.deinit();
        }
    };
    pub const Class = NewClass(
        Headers,
        .{
            .name = "Headers",
            .read_only = true,
        },
        .{
            .@"get" = .{
                .rfn = JS.get,
            },
            .@"set" = .{
                .rfn = JS.set,
                .ts = d.ts{},
            },
            .@"append" = .{
                .rfn = JS.append,
                .ts = d.ts{},
            },
            .@"delete" = .{
                .rfn = JS.delete,
                .ts = d.ts{},
            },
            .@"entries" = .{
                .rfn = JS.entries,
                .ts = d.ts{},
            },
            .@"keys" = .{
                .rfn = JS.keys,
                .ts = d.ts{},
            },
            .@"values" = .{
                .rfn = JS.values,
                .ts = d.ts{},
            },
            .@"constructor" = .{
                .rfn = JS.constructor,
                .ts = d.ts{},
            },
            .@"finalize" = .{
                .rfn = JS.finalize,
            },
        },
        .{},
    );

    // https://developer.mozilla.org/en-US/docs/Glossary/Guard
    pub const Guard = enum {
        immutable,
        request,
        @"request-no-cors",
        response,
        none,
    };

    pub fn fromPicoHeaders(allocator: *std.mem.Allocator, picohttp_headers: []const picohttp.Header) !Headers {
        var total_len: usize = 0;
        for (picohttp_headers) |header| {
            total_len += header.name.len;
            total_len += header.value.len;
        }
        // for the null bytes
        total_len += picohttp_headers.len * 2;
        var headers = Headers{
            .allocator = allocator,
            .entries = Entries{},
            .buf = std.ArrayListUnmanaged(u8){},
        };
        try headers.entries.ensureTotalCapacity(allocator, picohttp_headers.len);
        try headers.buf.ensureTotalCapacity(allocator, total_len);
        headers.buf.expandToCapacity();
        headers.guard = Guard.request;

        for (picohttp_headers) |header| {
            headers.entries.appendAssumeCapacity(Kv{
                .name = headers.appendString(
                    string,
                    header.name,
                    true,
                    true,
                    true,
                ),
                .value = headers.appendString(
                    string,
                    header.value,
                    true,
                    true,
                    true,
                ),
            });
        }

        return headers;
    }

    // TODO: is it worth making this lazy? instead of copying all the request headers, should we just do it on get/put/iterator?
    pub fn fromRequestCtx(allocator: *std.mem.Allocator, request: *http.RequestContext) !Headers {
        return fromPicoHeaders(allocator, request.request.headers);
    }

    pub fn asStr(headers: *const Headers, ptr: Api.StringPointer) []u8 {
        return headers.buf.items[ptr.offset..][0..ptr.length];
    }

    threadlocal var header_kv_buf: [4096]u8 = undefined;

    pub fn putHeader(headers: *Headers, key_: js.JSStringRef, value_: js.JSStringRef, comptime append: bool) void {
        const key_len = js.JSStringGetUTF8CString(key_, &header_kv_buf, header_kv_buf.len) - 1;
        // TODO: make this one pass instead of two
        var key = strings.trim(header_kv_buf[0..key_len], " \n\r");
        key = std.ascii.lowerString(key[0..key.len], key);

        var remainder = header_kv_buf[key.len..];
        const value_len = js.JSStringGetUTF8CString(value_, remainder.ptr, remainder.len) - 1;
        var value = strings.trim(remainder[0..value_len], " \n\r");

        if (headers.getHeaderIndex(key)) |header_i| {
            const existing_value = headers.entries.items(.value)[header_i];

            if (append) {
                const end = @truncate(u32, value.len + existing_value.length + 2);
                headers.buf.ensureUnusedCapacity(headers.allocator, end) catch unreachable;
                headers.buf.expandToCapacity();
                var new_end = headers.buf.items[headers.used..][0 .. end - 1];
                const existing_buf = headers.asStr(existing_value);
                std.mem.copy(u8, existing_buf, new_end);
                new_end[existing_buf.len] = ',';
                std.mem.copy(u8, new_end[existing_buf.len + 1 ..], value);
                new_end.ptr[end - 1] = 0;
                headers.entries.items(.value)[header_i] = Api.StringPointer{ .offset = headers.used, .length = end - 1 };
                headers.used += end;
                // Can we get away with just overwriting in-place?
            } else if (existing_value.length < value.len) {
                std.mem.copy(u8, headers.asStr(existing_value), value);
                headers.entries.items(.value)[header_i].length = @truncate(u32, value.len);
                headers.asStr(headers.entries.items(.value)[header_i]).ptr[value.len] = 0;
                // Otherwise, append to the buffer, and just don't bother dealing with the existing header value
                // We assume that these header objects are going to be kind of short-lived.
            } else {
                headers.buf.ensureUnusedCapacity(headers.allocator, value.len + 1) catch unreachable;
                headers.buf.expandToCapacity();
                headers.entries.items(.value)[header_i] = headers.appendString(string, value, false, true, true);
            }
        } else {
            headers.appendHeader(key, value, false, false, true);
        }
    }

    pub fn getHeaderIndex(headers: *const Headers, key: string) ?u32 {
        for (headers.entries.items(.name)) |name, i| {
            if (name.length == key.len and strings.eqlInsensitive(key, headers.asStr(name))) {
                return @truncate(u32, i);
            }
        }

        return null;
    }

    pub fn appendHeader(
        headers: *Headers,
        key: string,
        value: string,
        comptime needs_lowercase: bool,
        comptime needs_normalize: bool,
        comptime append_null: bool,
    ) void {
        headers.buf.ensureUnusedCapacity(headers.allocator, key.len + value.len + 2) catch unreachable;
        headers.buf.expandToCapacity();
        headers.entries.append(
            headers.allocator,
            Kv{
                .name = headers.appendString(
                    string,
                    key,
                    needs_lowercase,
                    needs_normalize,
                    append_null,
                ),
                .value = headers.appendString(
                    string,
                    value,
                    needs_lowercase,
                    needs_normalize,
                    append_null,
                ),
            },
        ) catch unreachable;
    }

    fn appendString(
        this: *Headers,
        comptime StringType: type,
        str: StringType,
        comptime needs_lowercase: bool,
        comptime needs_normalize: bool,
        comptime append_null: bool,
    ) Api.StringPointer {
        var ptr = Api.StringPointer{ .offset = this.used, .length = 0 };
        ptr.length = @truncate(
            u32,
            switch (comptime StringType) {
                js.JSStringRef => js.JSStringGetLength(str),
                else => str.len,
            },
        );
        std.debug.assert(ptr.length > 0);

        std.debug.assert(this.buf.items.len >= ptr.offset + ptr.length);
        var slice = this.buf.items[ptr.offset..][0..ptr.length];
        switch (comptime StringType) {
            js.JSStringRef => {
                ptr.length = @truncate(u32, js.JSStringGetUTF8CString(str, slice.ptr, slice.len) - 1);
            },
            else => {
                std.mem.copy(u8, slice, str);
            },
        }

        if (comptime needs_normalize) {
            slice = strings.trim(slice, " \r\n");
        }

        if (comptime needs_lowercase) {
            for (slice) |c, i| {
                slice[i] = std.ascii.toLower(c);
            }
        }
        if (comptime append_null) {
            slice.ptr[slice.len] = 0;
            this.used += 1;
        }

        ptr.length = @truncate(u32, slice.len);
        this.used += @truncate(u32, ptr.length);
        return ptr;
    }

    fn appendNumber(this: *Headers, num: f64) Api.StringPointer {
        var ptr = Api.StringPointer{ .offset = this.used, .length = @truncate(
            u32,
            std.fmt.count("{d}", .{num}),
        ) };
        std.debug.assert(this.buf.items.len >= ptr.offset + ptr.length);
        var slice = this.buf.items[ptr.offset..][0..ptr.length];
        var buf = std.fmt.bufPrint(slice, "{d}", .{num}) catch &[_]u8{};
        ptr.length = @truncate(u32, buf.len);
        this.used += ptr.length;
        return ptr;
    }

    pub fn appendInit(this: *Headers, ctx: js.JSContextRef, key: js.JSStringRef, comptime value_type: js.JSType, value: js.JSValueRef) !void {
        this.entries.append(this.allocator, Kv{
            .name = this.appendString(js.JSStringRef, key, true, true, false),
            .value = switch (comptime value_type) {
                js.JSType.kJSTypeNumber => this.appendNumber(js.JSValueToNumber(ctx, value, null)),
                js.JSType.kJSTypeString => this.appendString(js.JSStringRef, value, true, true, false),
                else => unreachable,
            },
        }) catch unreachable;
    }

    pub fn clone(this: *Headers, to: *Headers) !void {
        to.* = Headers{
            .entries = try this.entries.clone(this.allocator),
            .buf = try @TypeOf(this.buf).initCapacity(this.allocator, this.buf.items.len),
            .used = this.used,
            .allocator = this.allocator,
            .guard = Guard.none,
        };
        to.buf.expandToCapacity();
        std.mem.copy(u8, to.buf.items, this.buf.items);
    }
};

// https://developer.mozilla.org/en-US/docs/Web/API/Body
pub const Body = struct {
    init: Init,
    value: Value,
    ptr: ?[*]u8 = null,
    len: usize = 0,
    ptr_allocator: ?*std.mem.Allocator = null,

    pub fn deinit(this: *Body, allocator: *std.mem.Allocator) void {
        if (this.init.headers) |headers| {
            headers.deinit();
        }

        switch (this.value) {
            .ArrayBuffer => {},
            .String => |str| {
                allocator.free(str);
            },
            .Empty => {},
        }
    }

    pub const Init = struct {
        headers: ?Headers,
        status_code: u16,

        pub fn init(allocator: *std.mem.Allocator, ctx: js.JSContextRef, init_ref: js.JSValueRef) !?Init {
            var result = Init{ .headers = null, .status_code = 0 };
            var array = js.JSObjectCopyPropertyNames(ctx, init_ref);
            defer js.JSPropertyNameArrayRelease(array);
            const count = js.JSPropertyNameArrayGetCount(array);
            var i: usize = 0;
            while (i < count) : (i += 1) {
                var property_name_ref = js.JSPropertyNameArrayGetNameAtIndex(array, i);
                switch (js.JSStringGetLength(property_name_ref)) {
                    "headers".len => {
                        if (js.JSStringIsEqualToUTF8CString(property_name_ref, "headers")) {
                            // only support headers as an object for now.
                            if (js.JSObjectGetProperty(ctx, init_ref, property_name_ref, null)) |header_prop| {
                                switch (js.JSValueGetType(ctx, header_prop)) {
                                    js.JSType.kJSTypeObject => {
                                        result.headers = try Headers.JS.headersInit(ctx, header_prop);
                                    },
                                    else => {},
                                }
                            }
                        }
                    },
                    "statusCode".len => {
                        if (js.JSStringIsEqualToUTF8CString(property_name_ref, "statusCode")) {
                            var value_ref = js.JSObjectGetProperty(ctx, init_ref, property_name_ref, null);
                            var exception: js.JSValueRef = null;
                            const number = js.JSValueToNumber(ctx, value_ref, &exception);
                            if (exception != null or !std.math.isFinite(number)) continue;
                            result.status_code = @truncate(u16, @floatToInt(u64, number));
                        }
                    },
                    else => {},
                }
            }

            if (result.headers == null and result.status_code < 200) return null;
            return result;
        }
    };
    pub const Value = union(Tag) {
        ArrayBuffer: ArrayBuffer,
        String: string,
        Empty: u0,
        Unconsumed: u0,
        pub const Tag = enum {
            ArrayBuffer,
            String,
            Empty,
            Unconsumed,
        };

        pub fn length(value: *const Value) usize {
            switch (value.*) {
                .ArrayBuffer => |buf| {
                    return buf.ptr[buf.offset..buf.byte_len].len;
                },
                .String => |str| {
                    return str.len;
                },
                else => {
                    return 0;
                },
            }
        }
    };

    pub fn @"404"(ctx: js.JSContextRef) Body {
        return Body{ .init = Init{
            .headers = null,
            .status_code = 404,
        }, .value = .{ .Empty = 0 } };
    }

    pub fn extract(ctx: js.JSContextRef, body_ref: js.JSObjectRef, exception: js.ExceptionRef) Body {
        return extractBody(
            ctx,
            body_ref,
            false,
            null,
            exception,
        );
    }

    pub fn extractWithInit(ctx: js.JSContextRef, body_ref: js.JSObjectRef, init_ref: js.JSValueRef, exception: js.ExceptionRef) Body {
        return extractBody(
            ctx,
            body_ref,
            true,
            init_ref,
            exception,
        );
    }

    // https://github.com/WebKit/webkit/blob/main/Source/WebCore/Modules/fetch/FetchBody.cpp#L45
    inline fn extractBody(
        ctx: js.JSContextRef,
        body_ref: js.JSObjectRef,
        comptime has_init: bool,
        init_ref: js.JSValueRef,
        exception: js.ExceptionRef,
    ) Body {
        var body = Body{ .init = Init{ .headers = null, .status_code = 200 }, .value = .{ .Empty = 0 } };

        switch (js.JSValueGetType(ctx, body_ref)) {
            .kJSTypeString => {
                var allocator = getAllocator(ctx);

                if (comptime has_init) {
                    if (Init.init(allocator, ctx, init_ref.?)) |maybeInit| {
                        if (maybeInit) |init_| {
                            body.init = init_;
                        }
                    } else |err| {}
                }

                var wtf_string = JSValue.fromRef(body_ref).toWTFString(VirtualMachine.vm.global);

                if (wtf_string.isEmpty()) {
                    body.value = .{ .String = "" };
                    return body;
                }

                body.value = Value{
                    .String = wtf_string.characters8()[0..wtf_string.length()],
                };
                // body.ptr = body.
                // body.len = body.value.String.len;str.characters8()[0..len] };

                return body;
            },
            .kJSTypeObject => {
                const typed_array = js.JSValueGetTypedArrayType(ctx, body_ref, exception);
                switch (typed_array) {
                    js.JSTypedArrayType.kJSTypedArrayTypeNone => {},
                    else => {
                        const buffer = ArrayBuffer{
                            .ptr = @ptrCast([*]u8, js.JSObjectGetTypedArrayBytesPtr(ctx, body_ref.?, exception).?),
                            .offset = @truncate(u32, js.JSObjectGetTypedArrayByteOffset(ctx, body_ref.?, exception)),
                            .len = @truncate(u32, js.JSObjectGetTypedArrayLength(ctx, body_ref.?, exception)),
                            .byte_len = @truncate(u32, js.JSObjectGetTypedArrayLength(ctx, body_ref.?, exception)),
                            .typed_array_type = typed_array,
                        };
                        var allocator = getAllocator(ctx);

                        if (comptime has_init) {
                            if (Init.init(allocator, ctx, init_ref.?)) |maybeInit| {
                                if (maybeInit) |init_| {
                                    body.init = init_;
                                }
                            } else |err| {}
                        }
                        body.value = Value{ .ArrayBuffer = buffer };
                        body.ptr = buffer.ptr[buffer.offset..buffer.byte_len].ptr;
                        body.len = buffer.ptr[buffer.offset..buffer.byte_len].len;
                        return body;
                    },
                }
            },
            else => {},
        }

        if (exception == null) {
            JSError(getAllocator(ctx), "Body must be a string or a TypedArray (for now)", .{}, ctx, exception);
        }

        return body;
    }
};

// https://developer.mozilla.org/en-US/docs/Web/API/Request
pub const Request = struct {
    request_context: *http.RequestContext,
    url_string_ref: js.JSStringRef = null,
    headers: ?Headers = null,

    pub const Class = NewClass(
        Request,
        .{
            .name = "Request",
            .read_only = true,
        },
        .{},
        .{
            .@"cache" = .{
                .@"get" = getCache,
                .@"ro" = true,
            },
            .@"credentials" = .{
                .@"get" = getCredentials,
                .@"ro" = true,
            },
            .@"destination" = .{
                .@"get" = getDestination,
                .@"ro" = true,
            },
            .@"headers" = .{
                .@"get" = getHeaders,
                .@"ro" = true,
            },
            .@"integrity" = .{
                .@"get" = getIntegrity,
                .@"ro" = true,
            },
            .@"method" = .{
                .@"get" = getMethod,
                .@"ro" = true,
            },
            .@"mode" = .{
                .@"get" = getMode,
                .@"ro" = true,
            },
            .@"redirect" = .{
                .@"get" = getRedirect,
                .@"ro" = true,
            },
            .@"referrer" = .{
                .@"get" = getReferrer,
                .@"ro" = true,
            },
            .@"referrerPolicy" = .{
                .@"get" = getReferrerPolicy,
                .@"ro" = true,
            },
            .@"url" = .{
                .@"get" = getUrl,
                .@"ro" = true,
            },
        },
    );

    pub fn getCache(
        this: *Request,
        ctx: js.JSContextRef,
        thisObject: js.JSObjectRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        return js.JSValueMakeString(ctx, ZigString.init(Properties.UTF8.default).toValueGC(VirtualMachine.vm.global).asRef());
    }
    pub fn getCredentials(
        this: *Request,
        ctx: js.JSContextRef,
        thisObject: js.JSObjectRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        return js.JSValueMakeString(ctx, ZigString.init(Properties.UTF8.include).toValueGC(VirtualMachine.vm.global).asRef());
    }
    pub fn getDestination(
        this: *Request,
        ctx: js.JSContextRef,
        thisObject: js.JSObjectRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        return js.JSValueMakeString(ctx, ZigString.init("").toValueGC(VirtualMachine.vm.global).asRef());
    }
    pub fn getHeaders(
        this: *Request,
        ctx: js.JSContextRef,
        thisObject: js.JSObjectRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        if (this.headers == null) {
            this.headers = Headers.fromRequestCtx(getAllocator(ctx), this.request_context) catch unreachable;
        }

        return Headers.Class.make(ctx, &this.headers.?);
    }
    pub fn getIntegrity(
        this: *Request,
        ctx: js.JSContextRef,
        thisObject: js.JSObjectRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        return ZigString.Empty.toValueGC(VirtualMachine.vm.global).asRef();
    }
    pub fn getMethod(
        this: *Request,
        ctx: js.JSContextRef,
        thisObject: js.JSObjectRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        const string_contents: string = switch (this.request_context.method) {
            .GET => Properties.UTF8.GET,
            .HEAD => Properties.UTF8.HEAD,
            .PATCH => Properties.UTF8.PATCH,
            .PUT => Properties.UTF8.PUT,
            .POST => Properties.UTF8.POST,
            .OPTIONS => Properties.UTF8.OPTIONS,
            else => "",
        };

        return ZigString.init(string_contents).toValueGC(VirtualMachine.vm.global).asRef();
    }

    pub fn getMode(
        this: *Request,
        ctx: js.JSContextRef,
        thisObject: js.JSObjectRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        return ZigString.init(Properties.UTF8.navigate).toValueGC(VirtualMachine.vm.global).asRef();
    }
    pub fn getRedirect(
        this: *Request,
        ctx: js.JSContextRef,
        thisObject: js.JSObjectRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        return ZigString.init(Properties.UTF8.follow).toValueGC(VirtualMachine.vm.global).asRef();
    }
    pub fn getReferrer(
        this: *Request,
        ctx: js.JSContextRef,
        thisObject: js.JSObjectRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        if (this.request_context.header("Referrer")) |referrer| {
            return ZigString.init(referrer.value).toValueGC(VirtualMachine.vm.global).asRef();
        } else {
            return ZigString.init("").toValueGC(VirtualMachine.vm.global).asRef();
        }
    }
    pub fn getReferrerPolicy(
        this: *Request,
        ctx: js.JSContextRef,
        thisObject: js.JSObjectRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        return ZigString.init("").toValueGC(VirtualMachine.vm.global).asRef();
    }
    pub fn getUrl(
        this: *Request,
        ctx: js.JSContextRef,
        thisObject: js.JSObjectRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        if (this.url_string_ref == null) {
            this.url_string_ref = js.JSStringCreateWithUTF8CString(this.request_context.getFullURL());
        }

        return js.JSValueMakeString(ctx, this.url_string_ref);
    }
};

// https://github.com/WebKit/WebKit/blob/main/Source/WebCore/workers/service/FetchEvent.h
pub const FetchEvent = struct {
    started_waiting_at: u64 = 0,
    response: ?*Response = null,
    request_context: *http.RequestContext,
    request: Request,

    onPromiseRejectionCtx: *c_void = undefined,
    onPromiseRejectionHandler: ?fn (ctx: *c_void, err: anyerror, fetch_event: *FetchEvent, value: JSValue) void = null,
    rejected: bool = false,

    pub const Class = NewClass(
        FetchEvent,
        .{
            .name = "FetchEvent",
            .read_only = true,
            .ts = .{ .class = d.ts.class{ .interface = true } },
        },
        .{
            .@"respondWith" = .{
                .rfn = respondWith,
                .ts = d.ts{
                    .tsdoc = "Render the response in the active HTTP request",
                    .@"return" = "void",
                    .args = &[_]d.ts.arg{
                        .{ .name = "response", .@"return" = "Response" },
                    },
                },
            },
            .@"waitUntil" = waitUntil,
        },
        .{
            .@"client" = .{
                .@"get" = getClient,
                .ro = true,
                .ts = d.ts{
                    .tsdoc = "HTTP client metadata. This is not implemented yet, do not use.",
                    .@"return" = "undefined",
                },
            },
            .@"request" = .{
                .@"get" = getRequest,
                .ro = true,
                .ts = d.ts{
                    .tsdoc = "HTTP request",
                    .@"return" = "InstanceType<Request>",
                },
            },
        },
    );

    pub fn getClient(
        this: *FetchEvent,
        ctx: js.JSContextRef,
        thisObject: js.JSObjectRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        Output.prettyErrorln("FetchEvent.client is not implemented yet - sorry!!", .{});
        Output.flush();
        return js.JSValueMakeUndefined(ctx);
    }
    pub fn getRequest(
        this: *FetchEvent,
        ctx: js.JSContextRef,
        thisObject: js.JSObjectRef,
        prop: js.JSStringRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        return Request.Class.make(ctx, &this.request);
    }

    // https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith
    pub fn respondWith(
        this: *FetchEvent,
        ctx: js.JSContextRef,
        function: js.JSObjectRef,
        thisObject: js.JSObjectRef,
        arguments: []const js.JSValueRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        if (this.request_context.has_called_done) return js.JSValueMakeUndefined(ctx);

        // A Response or a Promise that resolves to a Response. Otherwise, a network error is returned to Fetch.
        if (arguments.len == 0 or !Response.Class.loaded) {
            JSError(getAllocator(ctx), "event.respondWith() must be a Response or a Promise<Response>.", .{}, ctx, exception);
            this.request_context.sendInternalError(error.respondWithWasEmpty) catch {};
            return js.JSValueMakeUndefined(ctx);
        }

        var resolved = JSInternalPromise.resolvedPromise(VirtualMachine.vm.global, JSValue.fromRef(arguments[0]));

        var status = resolved.status(VirtualMachine.vm.global.vm());

        if (status == .Pending) {
            VirtualMachine.vm.global.vm().drainMicrotasks();
        }

        status = resolved.status(VirtualMachine.vm.global.vm());

        switch (status) {
            .Fulfilled => {},
            else => {
                this.rejected = true;
                this.onPromiseRejectionHandler.?(
                    this.onPromiseRejectionCtx,
                    error.PromiseRejection,
                    this,
                    resolved.result(VirtualMachine.vm.global.vm()),
                );
                return js.JSValueMakeUndefined(ctx);
            },
        }

        var arg = resolved.result(VirtualMachine.vm.global.vm()).asObjectRef();

        if (!js.JSValueIsObjectOfClass(ctx, arg, Response.Class.ref)) {
            this.rejected = true;
            JSError(getAllocator(ctx), "event.respondWith() must be a Response or a Promise<Response>.", .{}, ctx, exception);
            this.onPromiseRejectionHandler.?(this.onPromiseRejectionCtx, error.RespondWithInvalidType, this, JSValue.fromRef(exception.*));
            return js.JSValueMakeUndefined(ctx);
        }

        var response: *Response = GetJSPrivateData(Response, arg) orelse {
            this.rejected = true;
            JSError(getAllocator(ctx), "event.respondWith()'s Response object was invalid. This may be an internal error.", .{}, ctx, exception);
            this.onPromiseRejectionHandler.?(this.onPromiseRejectionCtx, error.RespondWithInvalidTypeInternal, this, JSValue.fromRef(exception.*));
            return js.JSValueMakeUndefined(ctx);
        };

        defer {
            if (!VirtualMachine.vm.had_errors) {
                Output.printElapsed(@intToFloat(f64, (this.request_context.timer.lap())) / std.time.ns_per_ms);

                Output.prettyError(
                    " <b>/{s}<r><d> - <b>{d}<r> <d>transpiled, <d><b>{d}<r> <d>imports<r>\n",
                    .{
                        this.request_context.matched_route.?.name,
                        VirtualMachine.vm.transpiled_count,
                        VirtualMachine.vm.resolved_count,
                    },
                );
            }
        }

        var needs_mime_type = true;
        var content_length: ?usize = null;
        if (response.body.init.headers) |*headers| {
            this.request_context.clearHeaders() catch {};
            var i: usize = 0;
            while (i < headers.entries.len) : (i += 1) {
                var header = headers.entries.get(i);
                const name = headers.asStr(header.name);
                if (strings.eqlComptime(name, "content-type")) {
                    needs_mime_type = false;
                }

                if (strings.eqlComptime(name, "content-length")) {
                    content_length = std.fmt.parseInt(usize, headers.asStr(header.value), 10) catch null;
                    continue;
                }

                this.request_context.appendHeaderSlow(
                    name,
                    headers.asStr(header.value),
                ) catch unreachable;
            }
        }

        if (needs_mime_type) {
            this.request_context.appendHeader("Content-Type", response.mimeType(this.request_context));
        }

        const content_length_ = content_length orelse response.body.value.length();

        if (content_length_ == 0) {
            this.request_context.sendNoContent() catch return js.JSValueMakeUndefined(ctx);
            return js.JSValueMakeUndefined(ctx);
        }

        if (FeatureFlags.strong_etags_for_built_files) {
            switch (response.body.value) {
                .ArrayBuffer => |buf| {
                    const did_send = this.request_context.writeETag(buf.ptr[buf.offset..buf.byte_len]) catch false;
                    if (did_send) return js.JSValueMakeUndefined(ctx);
                },
                .String => |str| {
                    const did_send = this.request_context.writeETag(str) catch false;
                    if (did_send) {
                        // defer getAllocator(ctx).destroy(str.ptr);
                        return js.JSValueMakeUndefined(ctx);
                    }
                },
                else => unreachable,
            }
        }

        defer this.request_context.done();

        this.request_context.writeStatusSlow(response.body.init.status_code) catch return js.JSValueMakeUndefined(ctx);
        this.request_context.prepareToSendBody(content_length_, false) catch return js.JSValueMakeUndefined(ctx);

        switch (response.body.value) {
            .ArrayBuffer => |buf| {
                this.request_context.writeBodyBuf(buf.ptr[buf.offset..buf.byte_len]) catch return js.JSValueMakeUndefined(ctx);
            },
            .String => |str| {
                // defer getAllocator(ctx).destroy(str.ptr);
                this.request_context.writeBodyBuf(str) catch return js.JSValueMakeUndefined(ctx);
            },
            else => unreachable,
        }

        return js.JSValueMakeUndefined(ctx);
    }

    pub fn waitUntil(
        this: *FetchEvent,
        ctx: js.JSContextRef,
        function: js.JSObjectRef,
        thisObject: js.JSObjectRef,
        arguments: []const js.JSValueRef,
        exception: js.ExceptionRef,
    ) js.JSValueRef {
        return js.JSValueMakeUndefined(ctx);
    }
};

// pub const ReadableStream = struct {
//     pub const Class = NewClass(
//         ReadableStream,
//         .{
//             .name = "ReadableStream",
//         },
//         .{},
//         .{

//         },
//     );
// };

// pub const TextEncoder = struct {
//     pub const Class = NewClass(
//         TextEncoder,
//         .{
//             .name = "TextEncoder",
//         },
//         .{
//             .encoding = .{
//                 .@"get" = getEncoding,
//                 .ro = true,
//             },
//         },
//         .{
//             .encode = .{
//                 .rfn = encode,
//             },
//             .constructor = .{
//                 .rfn = constructor,
//             },
//             .encodeInto = .{
//                 .rfn = encodeInto,
//             },
//         },
//     );

//     const encoding_str = "utf-8";
//     pub fn getEncoding(
//         this: *TextEncoder,
//         ctx: js.JSContextRef,
//         thisObject: js.JSObjectRef,
//         prop: js.JSStringRef,
//         exception: js.ExceptionRef,
//     ) js.JSValueRef {
//         return ZigString.init(encoding_str).toValue(ctx).asRef()
//     }
// };

// pub const TextDecoder = struct {
//     pub const Class = NewClass(
//         TextDecoder,
//         .{
//             .name = "TextDecoder",
//         },
//         .{},
//         .{
//             .decode = .{},
//             .constructor = .{},
//         },
//     );
// };
