/*
 * Control plane: QuickJS runs bundled npm/app JS; data plane (fs/http TCP)
 * stays in C++ (jsc_node_io, js_node.h) and is reached only via FFI here.
 *
 * Built only with -DJSC_QJS_CONTROL_PLANE (see main.cpp -build).
 */

#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#endif
#include <quickjs.h>
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif

#undef JS_UNDEFINED
#undef JS_NULL
#undef JS_FALSE
#undef JS_TRUE
#undef JS_BOOL

#include "../js_value.h"
#include "../js_node.h"
#include "jsc_blue_qjs.h"
#include "jsc_runtime.h"

#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>

#if (defined(JSC_QJS_CONTROL_PLANE) || defined(JSC_HYBRID)) && \
    defined(JSC_RUNTIME_NODE_IO) && defined(JSC_HAVE_UV)

extern "C" JSValue jsc_quickjs_jsvalue_to_qjs(JsValue jv);

static void blue_write_boot_metric_ms(long long ms) {
    const char* p = std::getenv("BLUE_BOOT_METRICS_FILE");
    if (!p || !*p)
        return;
    std::fflush(stdout);
    std::fflush(stderr);
    if (FILE* f = std::fopen(p, "w")) {
        std::fprintf(f, "%lld\n", ms);
        std::fclose(f);
    }
}

static inline JSValue qjs_undef(void) {
    return JS_MKVAL(JS_TAG_UNDEFINED, 0);
}

static JSValue qjs_from_jsnode_builtin(JSContext* ctx, const char* spec) {
    (void)ctx;
    JsValue spec_v = js_str(spec ? spec : "");
    JsValue mod = js_node_require(spec_v);
    js_dispose_value(&spec_v);
    if (mod.tag == JS_UNDEFINED)
        return qjs_undef();
    JSValue qmod = jsc_quickjs_jsvalue_to_qjs(mod);
    js_dispose_value(&mod);
    return qmod;
}

/** QuickJS often has no `console`; island code uses this for real stderr output. */
static JSValue qjs_console_print_line(JSContext* ctx, JSValueConst this_val,
                                      int argc, JSValueConst* argv) {
    (void)this_val;
    const char* s = nullptr;
    if (argc >= 1)
        s = JS_ToCString(ctx, argv[0]);
    std::fprintf(stderr, "%s\n", s && s[0] ? s : "");
    std::fflush(stderr);
    if (s)
        JS_FreeCString(ctx, s);
    return qjs_undef();
}

struct QjsHttpHandler {
    JSContext* ctx;
    JSValue    fn;
};

static void free_qjs_http_handler(QjsHttpHandler* h) {
    if (!h)
        return;
    if (h->ctx && !JS_IsUndefined(h->fn) && !JS_IsNull(h->fn))
        JS_FreeValue(h->ctx, h->fn);
    std::free(h);
}

extern "C" void jsc_qjs_free_http_opaque(void* opaque) {
    free_qjs_http_handler((QjsHttpHandler*)opaque);
}

static JSValue __attribute__((unused))
qjs_fs_readFileSync(JSContext* ctx, JSValueConst this_val, int argc,
                    JSValueConst* argv) {
    (void)this_val;
    const char* path = nullptr;
    if (argc >= 1) {
        path = JS_ToCString(ctx, argv[0]);
        if (!path)
            return qjs_undef();
    }
    JsValue jpath = js_str(path ? path : "");
    if (path)
        JS_FreeCString(ctx, path);
    JsValue out = js_fs_readFileSync(jpath, JS_UNDEFINED());
    js_dispose_value(&jpath);
    JSValue qout = jsc_quickjs_jsvalue_to_qjs(out);
    js_dispose_value(&out);
    return qout;
}

static JSValue qjs_http_listen(JSContext* ctx, JSValueConst this_val, int argc,
                               JSValueConst* argv) {
    int sid = -1;
    {
        JSValue v = JS_GetPropertyStr(ctx, this_val, "__sid");
        if (!JS_IsException(v)) {
            int32_t x = 0;
            if (JS_ToInt32(ctx, &x, v) == 0)
                sid = (int)x;
            JS_FreeValue(ctx, v);
        }
    }
    if (sid < 0)
        return qjs_undef();
    int port = 8080;
    if (argc >= 1) {
        int32_t p = 0;
        if (JS_ToInt32(ctx, &p, argv[0]) == 0 && p > 0 && p <= 65535)
            port = (int)p;
    }
    /* Node: listen(port[, host][, backlog][, cb]); demos use listen(port, host, cb). */
    int cb_index = -1;
    if (argc >= 3 && JS_IsFunction(ctx, argv[2]))
        cb_index = 2;
    else if (argc >= 2 && JS_IsFunction(ctx, argv[1]))
        cb_index = 1;

    jsc_http_srv_listen_slot(sid, port);

    if (cb_index >= 0) {
        JSValue ret = JS_Call(ctx, argv[cb_index], this_val, 0, nullptr);
        if (JS_IsException(ret)) {
            JS_FreeValue(ctx, ret);
            return JS_EXCEPTION;
        }
        JS_FreeValue(ctx, ret);
    }
    return qjs_undef();
}

static JSValue qjs_http_createServer(JSContext* ctx, JSValueConst this_val,
                                     int argc, JSValueConst* argv) {
    (void)this_val;
    if (argc < 1 || !JS_IsFunction(ctx, argv[0]))
        return qjs_undef();

    int sid = jsc_http_srv_alloc_slot();
    if (sid < 0)
        return qjs_undef();

    auto* qh = (QjsHttpHandler*)std::malloc(sizeof(QjsHttpHandler));
    if (!qh) {
        jsc_http_srv_discard_unstarted(sid);
        return qjs_undef();
    }
    qh->ctx = ctx;
    qh->fn  = JS_DupValue(ctx, argv[0]);
    jsc_http_srv_set_qjs_opaque(sid, qh);

    JSValue srv = JS_NewObject(ctx);
    JS_SetPropertyStr(ctx, srv, "__sid", JS_NewInt32(ctx, sid));
    JS_SetPropertyStr(
        ctx, srv, "listen",
        JS_NewCFunction(ctx, qjs_http_listen, "listen", 4));
    return srv;
}

static JSValue qjs_express_module(JSContext* ctx) {
    static JSContext* s_ctx = nullptr;
    static JSValue s_mod = {};
    static bool s_has = false;
    if (s_ctx == ctx && s_has && !JS_IsUndefined(s_mod) && !JS_IsNull(s_mod))
        return JS_DupValue(ctx, s_mod);
    if (s_ctx != nullptr && s_has && !JS_IsUndefined(s_mod) && !JS_IsNull(s_mod))
        JS_FreeValue(s_ctx, s_mod);
    s_ctx = ctx;
    static const char* src =
        "(function(){\n"
        "  function parseQuery(qs){ var o={}; if(!qs) return o; var p=String(qs).split('&'); for(var i=0;i<p.length;i++){ if(!p[i]) continue; var kv=p[i].split('='); var k=decodeURIComponent(kv[0]||''); if(!k) continue; var v=decodeURIComponent(kv.slice(1).join('=')||''); o[k]=v; } return o; }\n"
        "  function toPath(url){ var q=String(url||'/'); var qi=q.indexOf('?'); return qi>=0?q.slice(0,qi):q; }\n"
        "  function parseReq(req){ req.path=toPath(req.url); var qi=String(req.url||'').indexOf('?'); req.query=parseQuery(qi>=0?String(req.url).slice(qi+1):''); }\n"
        "  function normPrefix(p){ if(!p) return '/'; p=String(p); if(p[0] !== '/') p='/'+p; if(p.length>1 && p.endsWith('/')) p=p.slice(0,-1); return p; }\n"
        "  function prefixMatch(path,pfx){ if(pfx==='/'||!pfx) return true; return path===pfx || path.indexOf(pfx + '/')===0; }\n"
        "  function createApp(){\n"
        "    var app=function(req,res,next){ return app.handle(req,res,next); };\n"
        "    app._middlewares=[]; app._routes=[]; app._errors=[];\n"
        "    app.use=function(a,b){ var p='/', fn=a; if(typeof a==='string'){ p=normPrefix(a); fn=b; } if(typeof fn==='function'){ if(fn.length===4) app._errors.push({p:p,fn:fn}); else app._middlewares.push({p:p,fn:fn}); } return app; };\n"
        "    function addRoute(m,path,fn){ app._routes.push({m:m,p:normPrefix(path||'/'),fn:fn}); return app; }\n"
        "    app.get=function(path,fn){ return addRoute('GET',path,fn); };\n"
        "    app.post=function(path,fn){ return addRoute('POST',path,fn); };\n"
        "    app.put=function(path,fn){ return addRoute('PUT',path,fn); };\n"
        "    app.patch=function(path,fn){ return addRoute('PATCH',path,fn); };\n"
        "    app.delete=function(path,fn){ return addRoute('DELETE',path,fn); };\n"
        "    app.options=function(path,fn){ return addRoute('OPTIONS',path,fn); };\n"
        "    app.head=function(path,fn){ return addRoute('HEAD',path,fn); };\n"
        "    app.handle=function(req,res,out){ req.url=String(req.url||'/'); parseReq(req); var i=0;\n"
        "      function final(err){ if(err){ res.statusCode=500; return res.end(String(err&&err.message||err)); } if(typeof out==='function') return out(); res.statusCode=404; return res.end('Cannot '+(req.method||'GET')+' '+(req.path||'/')); }\n"
        "      function runError(err,ei){ if(ei>=app._errors.length) return final(err); var e=app._errors[ei]; if(!prefixMatch(req.path,e.p)) return runError(err,ei+1); return e.fn(err,req,res,function(nerr){ runError(nerr||err,ei+1); }); }\n"
        "      function runRoute(){ var m=String(req.method||'GET').toUpperCase(); for(var r=0;r<app._routes.length;r++){ var rt=app._routes[r]; if(!rt) continue; if(rt.m!==m && !(m==='HEAD'&&rt.m==='GET')) continue; if(rt.p!==req.path) continue; return rt.fn(req,res,next); } return final(); }\n"
        "      function runMw(ent){ if(!prefixMatch(req.path,ent.p)) return next(); var prev=req.url, prevPath=req.path; if(ent.p!=='/' && req.url.indexOf(ent.p)===0){ req.url=req.url.slice(ent.p.length) || '/'; parseReq(req); } return ent.fn(req,res,function(err){ req.url=prev; req.path=prevPath; if(err) return next(err); return next(); }); }\n"
        "      function next(err){ if(err) return runError(err,0); if(i<app._middlewares.length) return runMw(app._middlewares[i++]); return runRoute(); }\n"
        "      return next(); };\n"
        "    app.listen=function(port,host,cb){ if(typeof host==='function'){ cb=host; host=undefined; } var s=require('http').createServer(app); s.listen(port,host,cb); return s; };\n"
        "    return app; }\n"
        "  function express(){ return createApp(); }\n"
        "  express.Router=function(){ return createApp(); };\n"
        "  express.json=function(){ return function(req,_res,next){ var b=String(req.rawBody||'').trim(); if(!b){ req.body=req.body||{}; return next(); } if(b[0]==='{'||b[0]==='['){ try{ req.body=JSON.parse(b); }catch(e){ return next(e); } } else { req.body={}; } return next(); }; };\n"
        "  express.urlencoded=function(){ return function(req,_res,next){ var ct=String((req.headers&&req.headers['content-type'])||'').toLowerCase(); var b=String(req.rawBody||''); if(ct.indexOf('application/x-www-form-urlencoded')>=0 || b.indexOf('=')>=0){ req.body=parseQuery(b); } else { req.body=req.body||{}; } return next(); }; };\n"
        "  express.static=function(){ return function(_req,_res,next){ return next(); }; };\n"
        "  return express;\n"
        "})()";
    JSValue ex = JS_Eval(ctx, src, std::strlen(src), "<blue-express-shim>",
                         JS_EVAL_TYPE_GLOBAL);
    if (JS_IsException(ex))
        return qjs_undef();
    s_mod = JS_DupValue(ctx, ex);
    s_has = true;
    return ex;
}

static JSValue qjs_require(JSContext* ctx, JSValueConst this_val, int argc,
                           JSValueConst* argv) {
    (void)this_val;
    if (argc < 1)
        return qjs_undef();
    const char* spec = JS_ToCString(ctx, argv[0]);
    if (!spec)
        return qjs_undef();
    JSValue mod = qjs_undef();
    if (std::strcmp(spec, "express") == 0) {
        mod = qjs_express_module(ctx);
    } else if (std::strcmp(spec, "http") == 0 || std::strcmp(spec, "node:http") == 0) {
        mod = JS_NewObject(ctx);
        JS_SetPropertyStr(
            ctx, mod, "createServer",
            JS_NewCFunction(ctx, qjs_http_createServer, "createServer", 1));
        static const char* methodsJs =
            "['GET','POST','PUT','PATCH','DELETE','HEAD','OPTIONS']";
        static const char* statusJs =
            "({"
            "200:'OK',201:'Created',204:'No Content',"
            "301:'Moved Permanently',302:'Found',304:'Not Modified',"
            "400:'Bad Request',401:'Unauthorized',403:'Forbidden',404:'Not Found',"
            "405:'Method Not Allowed',409:'Conflict',413:'Payload Too Large',"
            "415:'Unsupported Media Type',429:'Too Many Requests',"
            "500:'Internal Server Error',501:'Not Implemented',503:'Service Unavailable'"
            "})";
        JSValue methods = JS_Eval(ctx, methodsJs, std::strlen(methodsJs),
                                  "<blue-http-methods>", JS_EVAL_TYPE_GLOBAL);
        JSValue status = JS_Eval(ctx, statusJs, std::strlen(statusJs),
                                 "<blue-http-status>", JS_EVAL_TYPE_GLOBAL);
        if (!JS_IsException(methods))
            JS_SetPropertyStr(ctx, mod, "METHODS", methods);
        else
            JS_FreeValue(ctx, methods);
        if (!JS_IsException(status))
            JS_SetPropertyStr(ctx, mod, "STATUS_CODES", status);
        else
            JS_FreeValue(ctx, status);
        JSValue g = JS_GetGlobalObject(ctx);
        JSValue inMsg = JS_GetPropertyStr(ctx, g, "__jsc_http_IncomingMessage");
        JSValue srvRes = JS_GetPropertyStr(ctx, g, "__jsc_http_ServerResponse");
        JS_FreeValue(ctx, g);
        if (!JS_IsException(inMsg))
            JS_SetPropertyStr(ctx, mod, "IncomingMessage", inMsg);
        else
            JS_FreeValue(ctx, inMsg);
        if (!JS_IsException(srvRes))
            JS_SetPropertyStr(ctx, mod, "ServerResponse", srvRes);
        else
            JS_FreeValue(ctx, srvRes);
    } else if (std::strcmp(spec, "buffer") == 0 ||
               std::strcmp(spec, "node:buffer") == 0) {
        JSValue g = JS_GetGlobalObject(ctx);
        JSValue b = JS_GetPropertyStr(ctx, g, "Buffer");
        JS_FreeValue(ctx, g);
        if (!JS_IsException(b) && !JS_IsUndefined(b)) {
            mod = JS_NewObject(ctx);
            JS_SetPropertyStr(ctx, mod, "Buffer", JS_DupValue(ctx, b));
            JSValue v = JS_GetPropertyStr(ctx, b, "from");
            if (!JS_IsException(v) && !JS_IsUndefined(v))
                JS_SetPropertyStr(ctx, mod, "from", v);
            else
                JS_FreeValue(ctx, v);
            v = JS_GetPropertyStr(ctx, b, "alloc");
            if (!JS_IsException(v) && !JS_IsUndefined(v))
                JS_SetPropertyStr(ctx, mod, "alloc", v);
            else
                JS_FreeValue(ctx, v);
            v = JS_GetPropertyStr(ctx, b, "isBuffer");
            if (!JS_IsException(v) && !JS_IsUndefined(v))
                JS_SetPropertyStr(ctx, mod, "isBuffer", v);
            else
                JS_FreeValue(ctx, v);
            v = JS_GetPropertyStr(ctx, b, "byteLength");
            if (!JS_IsException(v) && !JS_IsUndefined(v))
                JS_SetPropertyStr(ctx, mod, "byteLength", v);
            else
                JS_FreeValue(ctx, v);
            v = JS_GetPropertyStr(ctx, b, "alloc");
            if (!JS_IsException(v) && !JS_IsUndefined(v))
                JS_SetPropertyStr(ctx, mod, "allocUnsafe", JS_DupValue(ctx, v));
            if (!JS_IsException(v) && !JS_IsUndefined(v))
                JS_SetPropertyStr(ctx, mod, "allocUnsafeSlow", v);
            else
                JS_FreeValue(ctx, v);
        }
        JS_FreeValue(ctx, b);
    } else if (std::strcmp(spec, "events") == 0 ||
               std::strcmp(spec, "node:events") == 0) {
        static const char* eventsShim =
            "({"
            "EventEmitter:(function(){"
            " function E(){this._e={};}"
            " E.prototype.on=function(n,f){this._e=this._e||{};(this._e[n]||(this._e[n]=[])).push(f);return this;};"
            " E.prototype.addListener=E.prototype.on;"
            " E.prototype.once=function(n,f){var self=this;function w(){self.removeListener(n,w);return f.apply(this,arguments);} return this.on(n,w);};"
            " E.prototype.removeListener=function(n,f){this._e=this._e||{};var a=this._e[n]||[];this._e[n]=a.filter(function(x){return x!==f;});return this;};"
            " E.prototype.emit=function(n){this._e=this._e||{};var a=this._e[n]||[];var args=[].slice.call(arguments,1);for(var i=0;i<a.length;i++)a[i].apply(this,args);return a.length>0;};"
            " return E;"
            " })()"
            "})";
        mod = JS_Eval(ctx, eventsShim, std::strlen(eventsShim), "<blue-events-shim>",
                      JS_EVAL_TYPE_GLOBAL);
        if (JS_IsException(mod))
            mod = qjs_undef();
    } else if (std::strcmp(spec, "util") == 0 ||
               std::strcmp(spec, "node:util") == 0) {
        static const char* utilShim =
            "({"
            "format:function(x){return String(x);},"
            "inspect:function(x){return String(x);},"
            "deprecate:function(fn){return fn;},"
            "inherits:function(ctor,superCtor){"
            " if(!ctor||!superCtor)return;"
            " ctor.super_=superCtor;"
            " ctor.prototype=Object.create(superCtor.prototype||{});"
            " ctor.prototype.constructor=ctor;"
            "}"
            "})";
        mod = JS_Eval(ctx, utilShim, std::strlen(utilShim), "<blue-util-shim>",
                      JS_EVAL_TYPE_GLOBAL);
        if (JS_IsException(mod))
            mod = qjs_undef();
    } else if (std::strcmp(spec, "string_decoder") == 0 ||
               std::strcmp(spec, "node:string_decoder") == 0) {
        static const char* sdShim =
            "({"
            "StringDecoder:(function(){"
            " function SD(enc){this.encoding=enc||'utf8';}"
            " SD.prototype.write=function(chunk){"
            "  if(chunk==null) return '';"
            "  if(typeof chunk==='string') return chunk;"
            "  if(chunk instanceof Uint8Array){"
            "    var s=''; for(var i=0;i<chunk.length;i++) s+=String.fromCharCode(chunk[i]&255); return s;"
            "  }"
            "  return String(chunk);"
            " };"
            " SD.prototype.end=function(chunk){ return this.write(chunk); };"
            " return SD;"
            "})()"
            "})";
        mod = JS_Eval(ctx, sdShim, std::strlen(sdShim), "<blue-string-decoder-shim>",
                      JS_EVAL_TYPE_GLOBAL);
        if (JS_IsException(mod))
            mod = qjs_undef();
    } else {
        const char* use = spec;
        if (std::strncmp(spec, "node:", 5) == 0)
            use = spec + 5;
        mod = qjs_from_jsnode_builtin(ctx, use);
        if (JS_IsUndefined(mod)) {
            fprintf(stderr, "blue: control plane: unsupported require('%s')\n",
                    spec);
            mod = qjs_undef();
        }
    }
    JS_FreeCString(ctx, spec);
    return mod;
}

static JSValue qjs_native_res_end(JSContext* ctx, JSValueConst this_val,
                                  int argc, JSValueConst* argv) {
    JSValue j = JS_GetPropertyStr(ctx, this_val, "__conn");
    int       ci = -1;
    if (!JS_IsException(j)) {
        int32_t x = 0;
        if (JS_ToInt32(ctx, &x, j) == 0)
            ci = (int)x;
        JS_FreeValue(ctx, j);
    }
    if (ci < 0)
        return qjs_undef();
    const char* body = "";
    char*       owned = nullptr;
    int32_t     statusCode = 200;
    std::string headersText;
    if (argc >= 1) {
        owned = (char*)JS_ToCString(ctx, argv[0]);
        if (owned)
            body = owned;
    }
    if (argc >= 2) {
        (void)JS_ToInt32(ctx, &statusCode, argv[1]);
        if (statusCode < 100 || statusCode > 999)
            statusCode = 200;
    }
    if (argc >= 3 && JS_IsObject(argv[2])) {
        JSPropertyEnum* props = nullptr;
        uint32_t propCount = 0;
        if (JS_GetOwnPropertyNames(ctx, &props, &propCount, argv[2],
                                   JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) {
            for (uint32_t i = 0; i < propCount; ++i) {
                JSValue keyVal = JS_AtomToString(ctx, props[i].atom);
                const char* key = JS_ToCString(ctx, keyVal);
                JSValue val = JS_GetProperty(ctx, argv[2], props[i].atom);
                const char* value = JS_ToCString(ctx, val);
                if (key && value && *key) {
                    headersText += key;
                    headersText += ": ";
                    headersText += value;
                    headersText += "\r\n";
                }
                if (value)
                    JS_FreeCString(ctx, value);
                if (key)
                    JS_FreeCString(ctx, key);
                JS_FreeValue(ctx, val);
                JS_FreeValue(ctx, keyVal);
                JS_FreeAtom(ctx, props[i].atom);
            }
            js_free(ctx, props);
        }
    }
    jsc_http_conn_res_send_cstr(
        ci, statusCode, headersText.empty() ? nullptr : headersText.c_str(), body);
    if (owned)
        JS_FreeCString(ctx, owned);
    return qjs_undef();
}

/* Binary-safe variant: extracts raw bytes from a Uint8Array/ArrayBuffer
 * and sends them without any string coercion or null-byte truncation. */
static JSValue qjs_native_res_binary_end(JSContext* ctx, JSValueConst this_val,
                                         int argc, JSValueConst* argv) {
    JSValue j = JS_GetPropertyStr(ctx, this_val, "__conn");
    int ci = -1;
    if (!JS_IsException(j)) {
        int32_t x = 0;
        if (JS_ToInt32(ctx, &x, j) == 0)
            ci = (int)x;
        JS_FreeValue(ctx, j);
    }
    if (ci < 0)
        return qjs_undef();

    const uint8_t* body_data = nullptr;
    size_t body_len = 0;
    int32_t statusCode = 200;
    std::string headersText;

    /* argv[0]: Uint8Array body */
    if (argc >= 1) {
        size_t byte_offset = 0, byte_length = 0, bytes_per_element = 0;
        JSValue ab = JS_GetTypedArrayBuffer(ctx, argv[0], &byte_offset,
                                            &byte_length, &bytes_per_element);
        if (!JS_IsException(ab)) {
            size_t ab_size = 0;
            body_data = JS_GetArrayBuffer(ctx, &ab_size, ab);
            fprintf(stderr, "DEBUG binary_end: ab_size=%zu byte_offset=%zu byte_length=%zu bpe=%zu data=%p\n",
                    ab_size, byte_offset, byte_length, bytes_per_element, (void*)body_data);
            if (body_data) {
                body_data += byte_offset;
                body_len = byte_length;
            }
            JS_FreeValue(ctx, ab);
        } else {
            fprintf(stderr, "DEBUG binary_end: JS_GetTypedArrayBuffer FAILED (not a typed array?)\n");
        }
    }
    /* argv[1]: statusCode */
    if (argc >= 2) {
        (void)JS_ToInt32(ctx, &statusCode, argv[1]);
        if (statusCode < 100 || statusCode > 999)
            statusCode = 200;
    }
    /* argv[2]: headers object */
    if (argc >= 3 && JS_IsObject(argv[2])) {
        JSPropertyEnum* props = nullptr;
        uint32_t propCount = 0;
        if (JS_GetOwnPropertyNames(ctx, &props, &propCount, argv[2],
                                   JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) {
            for (uint32_t i = 0; i < propCount; ++i) {
                JSValue keyVal = JS_AtomToString(ctx, props[i].atom);
                const char* key = JS_ToCString(ctx, keyVal);
                JSValue val = JS_GetProperty(ctx, argv[2], props[i].atom);
                const char* value = JS_ToCString(ctx, val);
                if (key && value && *key) {
                    headersText += key;
                    headersText += ": ";
                    headersText += value;
                    headersText += "\r\n";
                }
                if (value)
                    JS_FreeCString(ctx, value);
                if (key)
                    JS_FreeCString(ctx, key);
                JS_FreeValue(ctx, val);
                JS_FreeValue(ctx, keyVal);
                JS_FreeAtom(ctx, props[i].atom);
            }
            js_free(ctx, props);
        }
    }
    jsc_http_conn_res_send_binary(
        ci, statusCode, headersText.empty() ? nullptr : headersText.c_str(),
        body_data, body_len);
    return qjs_undef();
}

extern "C" void jsc_qjs_dispatch_http_conn(void* opaque, const char* method,
                                           const char* path, const char* body,
                                           const char* headers_text,
                                           int conn_index) {
    static thread_local int s_qjs_http_dispatch_depth;
    if (s_qjs_http_dispatch_depth > 0) {
        fprintf(stderr,
                "blue: nested QuickJS HTTP dispatch (depth=%d) %s %s\n",
                s_qjs_http_dispatch_depth, method ? method : "?",
                path ? path : "?");
        fflush(stderr);
        jsc_http_conn_res_send_cstr(conn_index, 500,
                                    "content-type: application/json; charset=utf-8\r\n",
                                    "{\"error\":\"nested_http_dispatch\"}");
        return;
    }
    ++s_qjs_http_dispatch_depth;

    auto* qh = (QjsHttpHandler*)opaque;
    if (!qh || !qh->ctx) {
        --s_qjs_http_dispatch_depth;
        return;
    }
    JSContext* ctx = qh->ctx;

    JSValue req = JS_NewObject(ctx);
    JS_SetPropertyStr(ctx, req, "method", JS_NewString(ctx, method));
    JS_SetPropertyStr(ctx, req, "url", JS_NewString(ctx, path));
    JS_SetPropertyStr(ctx, req, "rawBody",
                      JS_NewString(ctx, body ? body : ""));
    JS_SetPropertyStr(ctx, req, "httpVersionMajor", JS_NewInt32(ctx, 1));
    JS_SetPropertyStr(ctx, req, "httpVersionMinor", JS_NewInt32(ctx, 1));

    JSValue res = JS_NewObject(ctx);
    JS_SetPropertyStr(ctx, res, "__conn", JS_NewInt32(ctx, conn_index));
    JS_SetPropertyStr(ctx, res, "__nativeEnd",
                      JS_NewCFunction(ctx, qjs_native_res_end, "end", 3));
    JS_SetPropertyStr(ctx, res, "__nativeBinaryEnd",
                      JS_NewCFunction(ctx, qjs_native_res_binary_end, "binaryEnd", 3));

    JSValue g = JS_GetGlobalObject(ctx);
    JSValue patchFn = JS_GetPropertyStr(ctx, g, "__jsc_http_patch");
    JS_FreeValue(ctx, g);
    if (!JS_IsException(patchFn) && JS_IsFunction(ctx, patchFn)) {
        JSValue m = JS_NewString(ctx, method ? method : "GET");
        JSValue u = JS_NewString(ctx, path ? path : "/");
        JSValue b = JS_NewString(ctx, body ? body : "");
        JSValue h = JS_NewString(ctx, headers_text ? headers_text : "");
        JSValueConst pArgs[6] = {req, res, m, u, b, h};
        JSValue patched = JS_Call(ctx, patchFn, qjs_undef(), 6, pArgs);
        JS_FreeValue(ctx, m);
        JS_FreeValue(ctx, u);
        JS_FreeValue(ctx, b);
        JS_FreeValue(ctx, h);
        if (JS_IsException(patched)) {
            JSValue ex = JS_GetException(ctx);
            const char* msg = JS_ToCString(ctx, ex);
            fprintf(stderr, "blue: http patch error: %s\n", msg ? msg : "?");
            if (msg)
                JS_FreeCString(ctx, msg);
            JS_FreeValue(ctx, ex);
        }
        JS_FreeValue(ctx, patched);
    }
    JS_FreeValue(ctx, patchFn);

    JSValueConst args[2] = {req, res};
    JSValue      ret = JS_Call(ctx, qh->fn, qjs_undef(), 2, args);
    if (JS_IsException(ret)) {
        JSValue ex = JS_GetException(ctx);
        const char* msg = JS_ToCString(ctx, ex);
        fprintf(stderr, "blue: QuickJS http handler error: %s\n",
                msg ? msg : "?");
        JSValue st = JS_GetPropertyStr(ctx, ex, "stack");
        if (!JS_IsException(st) && !JS_IsUndefined(st)) {
            const char* s = JS_ToCString(ctx, st);
            if (s) {
                fprintf(stderr, "blue: QuickJS http handler stack: %s\n", s);
                JS_FreeCString(ctx, s);
            }
            JS_FreeValue(ctx, st);
        } else if (!JS_IsException(st)) {
            JS_FreeValue(ctx, st);
        }
        if (msg)
            JS_FreeCString(ctx, msg);
        JS_FreeValue(ctx, ex);
    }
    JS_FreeValue(ctx, ret);
    /* Drain microtasks before freeing req/res: Express may schedule work that
     * still reads the request (e.g. parseurl / routing) after the handler
     * returns but before the job queue runs. */
    jsc_quickjs_pump_microtasks();
    JS_FreeValue(ctx, req);
    JS_FreeValue(ctx, res);
    --s_qjs_http_dispatch_depth;
}

extern "C" void jsc_qjs_install_control_plane_shims(JSContext* ctx) {
    static const char* prologue =
        "if (typeof Error !== 'undefined') {\n"
        "  if (typeof Error.stackTraceLimit === 'undefined') Error.stackTraceLimit = 10;\n"
        "  if (typeof Error.prepareStackTrace === 'undefined') Error.prepareStackTrace = void 0;\n"
        "  if (typeof Error.captureStackTrace !== 'function') {\n"
        "    Error.captureStackTrace = function(obj){\n"
        "      if (!obj) return;\n"
        "      var fakeSite = {\n"
        "        getFileName: function(){ return 'blue-hybrid-island.js'; },\n"
        "        getFunctionName: function(){ return ''; },\n"
        "        getFunction: function(){ return null; },\n"
        "        getTypeName: function(){ return ''; },\n"
        "        getMethodName: function(){ return ''; },\n"
        "        getLineNumber: function(){ return 1; },\n"
        "        getColumnNumber: function(){ return 1; },\n"
        "        isEval: function(){ return false; },\n"
        "        isNative: function(){ return false; },\n"
        "        isToplevel: function(){ return true; },\n"
        "        toString: function(){ return 'blue-hybrid-island.js:1:1'; }\n"
        "      };\n"
        "      obj.stack = [fakeSite, fakeSite, fakeSite];\n"
        "    };\n"
        "  }\n"
        "}\n"
        "if (typeof globalThis.setImmediate !== 'function') {\n"
        "  var __jsc_si_id = 1;\n"
        "  var __jsc_si_live = {};\n"
        "  globalThis.setImmediate = function(cb){\n"
        "    var id = __jsc_si_id++;\n"
        "    __jsc_si_live[id] = 1;\n"
        "    Promise.resolve().then(function(){\n"
        "      if (!__jsc_si_live[id]) return;\n"
        "      delete __jsc_si_live[id];\n"
        "      if (typeof cb === 'function') cb();\n"
        "    });\n"
        "    return id;\n"
        "  };\n"
        "}\n"
        "if (typeof globalThis.clearImmediate !== 'function') {\n"
        "  globalThis.clearImmediate = function(id){\n"
        "    if (id != null) delete __jsc_si_live[id];\n"
        "  };\n"
        "}\n"
        "if (typeof globalThis.__blue_console_print_line === 'function') {\n"
        "  function __blue_fmtConsoleArgs(args){\n"
        "    var a='', i;\n"
        "    for (i=0;i<args.length;i++){ if(i) a+=' '; try{ a+=(args[i]==null?'':String(args[i])); }catch(_e){ a+='[Object]'; } }\n"
        "    return a;\n"
        "  }\n"
        "  globalThis.console = {\n"
        "    log:function(){ globalThis.__blue_console_print_line(__blue_fmtConsoleArgs(arguments)); },\n"
        "    error:function(){ globalThis.__blue_console_print_line(__blue_fmtConsoleArgs(arguments)); },\n"
        "    warn:function(){ globalThis.__blue_console_print_line(__blue_fmtConsoleArgs(arguments)); },\n"
        "    info:function(){ globalThis.__blue_console_print_line(__blue_fmtConsoleArgs(arguments)); }\n"
        "  };\n"
        "}\n";
    JSValue gPre = JS_GetGlobalObject(ctx);
    JS_SetPropertyStr(
        ctx, gPre, "__blue_console_print_line",
        JS_NewCFunction(ctx, qjs_console_print_line, "__blue_console_print_line", 1));
    JS_FreeValue(ctx, gPre);
    JSValue r = JS_Eval(ctx, prologue, std::strlen(prologue), "<blue-node-shims>",
                        JS_EVAL_TYPE_GLOBAL);
    if (JS_IsException(r)) {
        JSValue ex = JS_GetException(ctx);
        const char* msg = JS_ToCString(ctx, ex);
        fprintf(stderr, "blue: prologue error: %s\n", msg ? msg : "?");
        if (msg)
            JS_FreeCString(ctx, msg);
        JS_FreeValue(ctx, ex);
    }
    JS_FreeValue(ctx, r);

    JSValue g = JS_GetGlobalObject(ctx);
    JSValue processMod = qjs_from_jsnode_builtin(ctx, "process");
    if (!JS_IsUndefined(processMod) && !JS_IsException(processMod)) {
        JS_SetPropertyStr(ctx, g, "process", processMod);
        static const char* nextTickShim =
            "if (typeof process !== 'undefined') {\n"
            "  process.nextTick = function(cb){ Promise.resolve().then(function(){ if (typeof cb==='function') cb(); }); };\n"
            "}\n";
        JSValue ntr = JS_Eval(ctx, nextTickShim, std::strlen(nextTickShim),
                              "<blue-nexttick-shim>", JS_EVAL_TYPE_GLOBAL);
        JS_FreeValue(ctx, ntr);
    }
    JS_SetPropertyStr(ctx, g, "global", JS_DupValue(ctx, g));
    JS_SetPropertyStr(
        ctx, g, "require",
        JS_NewCFunction(ctx, qjs_require, "require", 1));
    JS_FreeValue(ctx, g);

    static const char* bufferShim =
        "if (typeof globalThis.Buffer !== 'function') {\n"
        "  function Buffer(arg, enc) { return Buffer.from(arg, enc); }\n"
        "  Buffer.from = function(arg) {\n"
        "    if (arg == null) return new Uint8Array(0);\n"
        "    if (arg instanceof Uint8Array) return arg;\n"
        "    var s = String(arg);\n"
        "    var a = new Uint8Array(s.length);\n"
        "    for (var i = 0; i < s.length; i++) a[i] = s.charCodeAt(i) & 255;\n"
        "    return a;\n"
        "  };\n"
        "  Buffer.alloc = function(n) {\n"
        "    n = n|0; if (n < 0) n = 0; return new Uint8Array(n);\n"
        "  };\n"
        "  Buffer.byteLength = function(v) { return String(v == null ? '' : v).length; };\n"
        "  Buffer.isBuffer = function(v) { return v instanceof Uint8Array; };\n"
        "  Buffer.prototype = Uint8Array.prototype;\n"
        "  globalThis.Buffer = Buffer;\n"
        "}\n";
    JSValue b = JS_Eval(ctx, bufferShim, std::strlen(bufferShim),
                        "<blue-buffer-shim>", JS_EVAL_TYPE_GLOBAL);
    if (JS_IsException(b)) {
        JSValue ex = JS_GetException(ctx);
        const char* msg = JS_ToCString(ctx, ex);
        fprintf(stderr, "blue: buffer shim error: %s\n", msg ? msg : "?");
        if (msg)
            JS_FreeCString(ctx, msg);
        JS_FreeValue(ctx, ex);
    }
    JS_FreeValue(ctx, b);

    static const char* httpCompatShim =
        "if (!globalThis.__jsc_http_patch) {\n"
        "  function __JscEE(){ this.__ev = {}; }\n"
        "  __JscEE.prototype.on = function(n,f){ this.__ev=this.__ev||{}; (this.__ev[n]||(this.__ev[n]=[])).push(f); return this; };\n"
        "  __JscEE.prototype.addListener = __JscEE.prototype.on;\n"
        "  __JscEE.prototype.once = function(n,f){ var self=this; function w(){ self.removeListener(n,w); return f.apply(this, arguments);} return this.on(n,w); };\n"
        "  __JscEE.prototype.removeListener = function(n,f){ this.__ev=this.__ev||{}; var a=this.__ev[n]||[]; this.__ev[n]=a.filter(function(x){ return x!==f;}); return this; };\n"
        "  __JscEE.prototype.listeners = function(n){ this.__ev=this.__ev||{}; return (this.__ev[n]||[]).slice(); };\n"
        "  __JscEE.prototype.emit = function(n){ this.__ev=this.__ev||{}; var a=this.__ev[n]||[]; var args=[].slice.call(arguments,1); for(var i=0;i<a.length;i++) a[i].apply(this,args); return a.length>0; };\n"
        "  function __mkSocket(){ var s=new __JscEE(); s.readable=true; s.writable=true; s.destroyed=false; s.encrypted=false; s.setTimeout=function(){return s;}; s.destroy=function(){s.destroyed=true; s.emit('close');}; return s; }\n"
        "  function IncomingMessage(){ __JscEE.call(this); this.headers={}; this.rawHeaders=[]; this.trailers={}; this.httpVersion='1.1'; this.httpVersionMajor=1; this.httpVersionMinor=1; this.complete=true; this.aborted=false; this.readable=true; this.socket=__mkSocket(); this.connection=this.socket; }\n"
        "  IncomingMessage.prototype=Object.create(__JscEE.prototype); IncomingMessage.prototype.constructor=IncomingMessage;\n"
        "  IncomingMessage.prototype.pipe=function(dst){ this.on('data', function(c){ if(dst&&dst.write) dst.write(c); }); this.on('end', function(){ if(dst&&dst.end) dst.end(); }); return dst||this; };\n"
        "  IncomingMessage.prototype.unpipe=function(){ return this; };\n"
        "  IncomingMessage.prototype.pause=function(){ return this; };\n"
        "  IncomingMessage.prototype.resume=function(){ this.emit('resume'); this.emit('end'); return this; };\n"
        "  IncomingMessage.prototype.setTimeout=function(){ return this; };\n"
        "  function ServerResponse(){ __JscEE.call(this); this.statusCode=200; this.statusMessage='OK'; this.headersSent=false; this.finished=false; this.writableEnded=false; this._headers={}; this._body=''; this.socket=null; this.connection=null; }\n"
        "  ServerResponse.prototype=Object.create(__JscEE.prototype); ServerResponse.prototype.constructor=ServerResponse;\n"
        "  ServerResponse.prototype.setHeader=function(k,v){ this._headers[String(k).toLowerCase()] = String(v); return this; };\n"
        "  ServerResponse.prototype.getHeader=function(k){ return this._headers[String(k).toLowerCase()]; };\n"
        "  ServerResponse.prototype.removeHeader=function(k){ delete this._headers[String(k).toLowerCase()]; return this; };\n"
        "  ServerResponse.prototype.getHeaders=function(){ return this._headers; };\n"
        "  ServerResponse.prototype.writeHead=function(code,reason,hdrs){ this.statusCode=(code|0)||200; if(typeof reason==='object'&&reason) hdrs=reason; if(hdrs&&typeof hdrs==='object'){ for(var k in hdrs) this.setHeader(k,hdrs[k]); } this.headersSent=true; return this; };\n"
        "  ServerResponse.prototype.write=function(chunk){ this.headersSent=true; if(chunk!=null){ this._bodyChunks=this._bodyChunks||[]; this._bodyChunks.push(chunk); if(chunk&&typeof chunk==='object'&&typeof chunk.byteLength==='number') this._hasBinary=true; } this.emit('drain'); return true; };\n"
        "  ServerResponse.prototype.end=function(chunk){ if(chunk!=null) this.write(chunk); this.headersSent=true; this.finished=true; this.writableEnded=true; var chunks=this._bodyChunks||[]; if(this._hasBinary){ var t=0; for(var i=0;i<chunks.length;i++){ var c=chunks[i]; t+=(c&&typeof c==='object'&&typeof c.byteLength==='number')?c.byteLength:String(c).length; } var out=new Uint8Array(t),off=0; for(var i=0;i<chunks.length;i++){ var c=chunks[i]; if(c&&typeof c==='object'&&typeof c.byteLength==='number'){ out.set(new Uint8Array(c.buffer||c,c.byteOffset||0,c.byteLength),off); off+=c.byteLength; }else{ var s=String(c); for(var j=0;j<s.length;j++) out[off++]=s.charCodeAt(j)&0xff; } } if(typeof this.__nativeBinaryEnd==='function') this.__nativeBinaryEnd(out,this.statusCode,this._headers); } else { var body=''; for(var i=0;i<chunks.length;i++) body+=String(chunks[i]); if(typeof this.__nativeEnd==='function') this.__nativeEnd(body,this.statusCode,this._headers); } this.emit('finish'); if(this.socket&&this.socket.emit) this.socket.emit('close'); this.emit('close'); return this; };\n"
        "  ServerResponse.prototype.status=function(code){ this.statusCode=code|0; return this; };\n"
        "  ServerResponse.prototype.send=function(body){ if(body&&typeof body==='object'&&!(body instanceof Uint8Array)){ this.setHeader('content-type','application/json; charset=utf-8'); body=JSON.stringify(body); } this.end(body==null?'':body); return this; };\n"
        "  ServerResponse.prototype.json=function(obj){ this.setHeader('content-type','application/json; charset=utf-8'); this.end(JSON.stringify(obj==null?{}:obj)); return this; };\n"
        "  ServerResponse.prototype.setTimeout=function(){ return this; };\n"
        "  globalThis.__jsc_http_IncomingMessage = IncomingMessage;\n"
        "  globalThis.__jsc_http_ServerResponse = ServerResponse;\n"
        "  function __parseHeaders(text){\n"
        "    var headers={}; var raw=[];\n"
        "    if(!text) return {headers:headers,raw:raw};\n"
        "    var lines=String(text).split('\\r\\n');\n"
        "    for(var i=0;i<lines.length;i++){\n"
        "      var ln=lines[i]; if(!ln) continue;\n"
        "      var p=ln.indexOf(':'); if(p<=0) continue;\n"
        "      var k=ln.slice(0,p).trim().toLowerCase();\n"
        "      var v=ln.slice(p+1).trim();\n"
        "      if(!k) continue;\n"
        "      raw.push(k, v);\n"
        "      if(headers[k]===undefined) headers[k]=v;\n"
        "      else if(Array.isArray(headers[k])) headers[k].push(v);\n"
        "      else headers[k]=[headers[k], v];\n"
        "    }\n"
        "    return {headers:headers,raw:raw};\n"
        "  }\n"
        "  function __normalizeUrl(u){\n"
        "    var s=String(u||'/');\n"
        "    if(s.indexOf('http://')===0||s.indexOf('https://')===0){\n"
        "      var slash=s.indexOf('/', s.indexOf('://')+3);\n"
        "      s = slash>=0 ? s.slice(slash) : '/';\n"
        "    }\n"
        "    return s || '/';\n"
        "  }\n"
        "  globalThis.__jsc_http_patch = function(req,res,method,url,body,headersText){\n"
        "    Object.setPrototypeOf(req, IncomingMessage.prototype); IncomingMessage.call(req);\n"
        "    var parsed=__parseHeaders(headersText);\n"
        "    req.method = String(method || 'GET').toUpperCase(); req.url = __normalizeUrl(url); req.originalUrl=req.url; req.baseUrl='';\n"
        "    req.headers = parsed.headers; req.rawHeaders = parsed.raw;\n"
        "    req._parsedUrl = undefined;\n"
        "    req._parsedOriginalUrl = undefined;\n"
        "    req.rawBody = body || ''; req.body = undefined;\n"
        "    req.readableEnded = false;\n"
        "    req.complete = false;\n"
        "    var __ended = false;\n"
        "    req.resume = function(){\n"
        "      if (__ended) return req;\n"
        "      __ended = true;\n"
        "      if (req.rawBody && req.rawBody.length) req.emit('data', req.rawBody);\n"
        "      req.readable = false;\n"
        "      req.readableEnded = true;\n"
        "      req.complete = true;\n"
        "      req.emit('end');\n"
        "      return req;\n"
        "    };\n"
        "    Object.setPrototypeOf(res, ServerResponse.prototype); ServerResponse.call(res);\n"
        "    res.req=req; res.socket=req.socket; res.connection=req.connection; res.locals={};\n"
        "    setImmediate(function(){ req.resume(); });\n"
        "    return [req,res];\n"
        "  };\n"
        "}\n";
    JSValue h = JS_Eval(ctx, httpCompatShim, std::strlen(httpCompatShim),
                        "<blue-http-compat>", JS_EVAL_TYPE_GLOBAL);
    if (JS_IsException(h)) {
        JSValue ex = JS_GetException(ctx);
        const char* msg = JS_ToCString(ctx, ex);
        fprintf(stderr, "blue: http compat shim error: %s\n", msg ? msg : "?");
        if (msg)
            JS_FreeCString(ctx, msg);
        JS_FreeValue(ctx, ex);
    }
    JS_FreeValue(ctx, h);

    jsc_qjs_install_blue_desktop(ctx);
}

#ifdef JSC_QJS_CONTROL_PLANE
extern "C" void jsc_control_plane_run_embedded(const unsigned char* bundle,
                                               size_t len) {
    auto tCtl0 = std::chrono::steady_clock::now();
    jsc_quickjs_ensure_runtime();
    JSContext* ctx = jsc_quickjs_get_context();
    if (!ctx || !bundle || len == 0) {
        fprintf(stderr, "blue: control plane: missing context or bundle\n");
        return;
    }

    jsc_qjs_install_control_plane_shims(ctx);

    JSValue ret = JS_Eval(ctx, (const char*)bundle, len, "blue-npm-bundle.js",
                          JS_EVAL_TYPE_GLOBAL);
    if (JS_IsException(ret)) {
        JSValue ex = JS_GetException(ctx);
        const char* msg = JS_ToCString(ctx, ex);
        fprintf(stderr, "blue: bundle eval error: %s\n", msg ? msg : "?");
        if (msg)
            JS_FreeCString(ctx, msg);
        JS_FreeValue(ctx, ex);
    }
    JS_FreeValue(ctx, ret);
    jsc_quickjs_pump_microtasks();
    auto tCtl1          = std::chrono::steady_clock::now();
    long long elapsedMs = (long long)std::chrono::duration_cast<
        std::chrono::milliseconds>(tCtl1 - tCtl0)
                              .count();
    blue_write_boot_metric_ms(elapsedMs);
}
#endif /* JSC_QJS_CONTROL_PLANE */

#elif defined(JSC_QJS_CONTROL_PLANE) || defined(JSC_HYBRID)

extern "C" void jsc_qjs_free_http_opaque(void* opaque) {
    if (opaque) {
        auto* qh = (QjsHttpHandler*)opaque;
        if (qh->ctx)
            JS_FreeValue(qh->ctx, qh->fn);
        std::free(qh);
    }
}

#ifdef JSC_QJS_CONTROL_PLANE
extern "C" void jsc_control_plane_run_embedded(const unsigned char* bundle,
                                               size_t len) {
    (void)bundle;
    (void)len;
    fprintf(stderr,
            "blue: control plane requires JSC_RUNTIME_NODE_IO and "
            "JSC_HAVE_UV\n");
}
#endif

#endif
