///|
/// QuickJS Runtime for BiDi Script Execution
///
/// Uses quickjs-emscripten with ASYNCIFY for sandboxed JavaScript execution
/// with ES Modules support and network fetch capability.

///|
/// Global QuickJS state
priv struct QuickJSState {
  mut initialized : Bool
  mut pending_init : Bool
}

///|
let quickjs_state : QuickJSState = { initialized: false, pending_init: false }

///|
/// FFI: Initialize QuickJS ASYNCIFY runtime
/// Returns a Promise that resolves when initialization is complete
extern "js" fn quickjs_init_runtime() -> @core.Any =
  #| async () => {
  #|   if (globalThis.__quickjs_runtime) {
  #|     return globalThis.__quickjs_runtime;
  #|   }
  #|
  #|   // Import quickjs-emscripten
  #|   const { newQuickJSAsyncWASMModuleFromVariant } = await import("npm:quickjs-emscripten");
  #|   const releaseVariant = (await import("npm:@jitl/quickjs-wasmfile-release-asyncify")).default;
  #|
  #|   console.log("[QuickJS] Loading ASYNCIFY variant...");
  #|   const QuickJS = await newQuickJSAsyncWASMModuleFromVariant(releaseVariant);
  #|   console.log("[QuickJS] Runtime loaded");
  #|
  #|   const runtime = QuickJS.newRuntime();
  #|   const vm = runtime.newContext();
  #|
  #|   // Set up async module loader with fetch support
  #|   runtime.setModuleLoader(async (moduleName, ctx) => {
  #|     let url = moduleName;
  #|
  #|     // Resolve URLs
  #|     if (moduleName.startsWith("https://") || moduleName.startsWith("http://")) {
  #|       url = moduleName;
  #|     } else if (moduleName.startsWith("/")) {
  #|       // Root-relative URLs (from esm.sh redirects)
  #|       url = "https://esm.sh" + moduleName;
  #|     } else {
  #|       // Bare specifiers
  #|       url = "https://esm.sh/" + moduleName;
  #|     }
  #|
  #|     console.log("[QuickJS] Loading module:", url);
  #|
  #|     try {
  #|       const res = await fetch(url);
  #|       if (!res.ok) {
  #|         return { error: new Error("HTTP " + res.status + ": " + url) };
  #|       }
  #|       const code = await res.text();
  #|       return { value: code };
  #|     } catch (e) {
  #|       return { error: new Error("Fetch failed: " + e.message) };
  #|     }
  #|   });
  #|
  #|   // Store globally
  #|   globalThis.__quickjs_runtime = { QuickJS, runtime, vm };
  #|
  #|   // Setup Mock DOM in QuickJS context
  #|   await setupQuickJSMockDOM(vm);
  #|
  #|   return globalThis.__quickjs_runtime;
  #| }

///|
/// FFI: Evaluate expression in QuickJS context (sync version)
/// Returns BiDi-formatted result JSON with console entries
extern "js" fn quickjs_evaluate_sync(expression : String) -> String =
  #| (expression) => {
  #|   if (!globalThis.__quickjs_runtime) {
  #|     return JSON.stringify({
  #|       type: "exception",
  #|       exceptionDetails: {
  #|         text: "QuickJS not initialized",
  #|         exception: { type: "error" }
  #|       },
  #|       consoleEntries: []
  #|     });
  #|   }
  #|
  #|   // Note: sync evaluation with ASYNCIFY is not possible
  #|   // This returns a pending state - use async version instead
  #|   return JSON.stringify({
  #|     type: "exception",
  #|     exceptionDetails: {
  #|       text: "Use async evaluation for QuickJS",
  #|       exception: { type: "error" }
  #|     },
  #|     consoleEntries: []
  #|   });
  #| }

///|
/// FFI: Evaluate expression in QuickJS context (async version)
/// Returns Promise that resolves to BiDi-formatted result JSON with console entries
extern "js" fn quickjs_evaluate_async(
  expression : String,
  await_promise : Bool,
) -> @core.Any =
  #| async (expression, awaitPromise) => {
  #|   const rt = globalThis.__quickjs_runtime;
  #|   if (!rt || !rt.vm) {
  #|     return JSON.stringify({
  #|       type: "exception",
  #|       exceptionDetails: {
  #|         text: "QuickJS not initialized",
  #|         exception: { type: "error" }
  #|       },
  #|       consoleEntries: []
  #|     });
  #|   }
  #|   const { vm } = rt;
  #|
  #|   // Convert value to BiDi remote value
  #|   const toBidiValue = (value, depth = 0) => {
  #|     if (depth > 10) return { type: "object" }; // Prevent infinite recursion
  #|     if (value === undefined) return { type: "undefined" };
  #|     if (value === null) return { type: "null" };
  #|
  #|     const type = typeof value;
  #|
  #|     if (type === "boolean") return { type: "boolean", value };
  #|     if (type === "number") {
  #|       if (Number.isNaN(value)) return { type: "number", value: "NaN" };
  #|       if (!Number.isFinite(value)) return { type: "number", value: value > 0 ? "Infinity" : "-Infinity" };
  #|       if (Object.is(value, -0)) return { type: "number", value: "-0" };
  #|       return { type: "number", value };
  #|     }
  #|     if (type === "string") return { type: "string", value };
  #|     if (type === "bigint") return { type: "bigint", value: String(value) };
  #|     if (type === "symbol") return { type: "symbol" };
  #|     if (type === "function") return { type: "function" };
  #|
  #|     if (Array.isArray(value)) {
  #|       return { type: "array", value: value.map(v => toBidiValue(v, depth + 1)) };
  #|     }
  #|
  #|     if (value instanceof Date) {
  #|       return { type: "date", value: value.toISOString() };
  #|     }
  #|
  #|     if (value instanceof RegExp) {
  #|       return { type: "regexp", value: { pattern: value.source, flags: value.flags } };
  #|     }
  #|
  #|     if (value instanceof Error) {
  #|       return { type: "error", value: { type: value.name, message: value.message } };
  #|     }
  #|
  #|     // Object
  #|     if (type === "object") {
  #|       try {
  #|         const entries = Object.entries(value).map(([k, v]) => [k, toBidiValue(v, depth + 1)]);
  #|         return { type: "object", value: entries };
  #|       } catch (e) {
  #|         return { type: "object" };
  #|       }
  #|     }
  #|
  #|     return { type: "undefined" };
  #|   };
  #|
  #|   try {
  #|     const result = await vm.evalCodeAsync(expression, "eval.js");
  #|
  #|     // Collect console entries from QuickJS context
  #|     let consoleEntries = [];
  #|     try {
  #|       const logsResult = vm.evalCode("JSON.stringify(globalThis.console._logs || [])");
  #|       if (!logsResult.error) {
  #|         const logsJson = vm.dump(logsResult.value);
  #|         logsResult.value.dispose();
  #|         consoleEntries = JSON.parse(logsJson).map(log => ({
  #|           level: log.level === 'log' ? 'info' : log.level,
  #|           method: log.level,
  #|           text: log.args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' '),
  #|           timestamp: Date.now()
  #|         }));
  #|         // Clear the logs
  #|         const clearResult = vm.evalCode("globalThis.console._logs = []");
  #|         if (!clearResult.error) clearResult.value.dispose();
  #|         else if (clearResult.error) clearResult.error.dispose();
  #|       } else {
  #|         logsResult.error.dispose();
  #|       }
  #|     } catch (e) {
  #|       // Ignore console collection errors
  #|     }
  #|
  #|     if (result.error) {
  #|       const error = vm.dump(result.error);
  #|       result.error.dispose();
  #|       return JSON.stringify({
  #|         type: "exception",
  #|         exceptionDetails: {
  #|           columnNumber: 0,
  #|           lineNumber: 0,
  #|           text: typeof error === "string" ? error : JSON.stringify(error),
  #|           exception: { type: "error", value: { type: error?.name || "Error" } },
  #|           stackTrace: { callFrames: [] }
  #|         },
  #|         consoleEntries
  #|       });
  #|     }
  #|
  #|     const value = vm.dump(result.value);
  #|     result.value.dispose();
  #|
  #|     return JSON.stringify({
  #|       type: "success",
  #|       result: toBidiValue(value),
  #|       consoleEntries
  #|     });
  #|   } catch (e) {
  #|     return JSON.stringify({
  #|       type: "exception",
  #|       exceptionDetails: {
  #|         columnNumber: 0,
  #|         lineNumber: 0,
  #|         text: String(e),
  #|         exception: { type: "error", value: { type: e?.name || "Error" } },
  #|         stackTrace: { callFrames: [] }
  #|       },
  #|       consoleEntries: []
  #|     });
  #|   }
  #| }

///|
/// FFI: Evaluate ES module in QuickJS context
extern "js" fn quickjs_evaluate_module(code : String) -> @core.Any =
  #| async (code) => {
  #|   const { vm } = globalThis.__quickjs_runtime;
  #|   if (!vm) {
  #|     return JSON.stringify({
  #|       type: "exception",
  #|       exceptionDetails: { text: "QuickJS not initialized" }
  #|     });
  #|   }
  #|
  #|   try {
  #|     const result = await vm.evalCodeAsync(code, "module.js", { type: "module" });
  #|
  #|     if (result.error) {
  #|       const error = vm.dump(result.error);
  #|       result.error.dispose();
  #|       return JSON.stringify({
  #|         type: "exception",
  #|         exceptionDetails: { text: JSON.stringify(error) }
  #|       });
  #|     }
  #|
  #|     result.value.dispose();
  #|     return JSON.stringify({ type: "success", result: { type: "undefined" } });
  #|   } catch (e) {
  #|     return JSON.stringify({
  #|       type: "exception",
  #|       exceptionDetails: { text: String(e) }
  #|     });
  #|   }
  #| }

///|
/// FFI: Load HTML into QuickJS Mock DOM
extern "js" fn quickjs_load_html(html : String) -> @core.Any =
  #| async (html) => {
  #|   const { vm } = globalThis.__quickjs_runtime;
  #|   if (!vm) {
  #|     return JSON.stringify({ success: false, error: "QuickJS not initialized" });
  #|   }
  #|
  #|   // Simple HTML parser for QuickJS
  #|   const parserCode = `
  #|     (function(html) {
  #|       // Reset document
  #|       document.head._children = [];
  #|       document.body._children = [];
  #|
  #|       // Very simple HTML parsing
  #|       const tagRegex = /<(\\/?)(\\w+)([^>]*)>/g;
  #|       const attrRegex = /(\\w+)=["']([^"']*)["']/g;
  #|
  #|       let current = document.body;
  #|       let lastIndex = 0;
  #|       let match;
  #|
  #|       while ((match = tagRegex.exec(html)) !== null) {
  #|         // Add text content before this tag
  #|         const textBefore = html.slice(lastIndex, match.index).trim();
  #|         if (textBefore) {
  #|           current.appendChild(document.createTextNode(textBefore));
  #|         }
  #|         lastIndex = tagRegex.lastIndex;
  #|
  #|         const isClosing = match[1] === '/';
  #|         const tagName = match[2].toLowerCase();
  #|         const attrs = match[3];
  #|
  #|         if (isClosing) {
  #|           if (current._parent) current = current._parent;
  #|         } else {
  #|           const elem = document.createElement(tagName);
  #|
  #|           // Parse attributes
  #|           let attrMatch;
  #|           while ((attrMatch = attrRegex.exec(attrs)) !== null) {
  #|             elem.setAttribute(attrMatch[1], attrMatch[2]);
  #|           }
  #|           attrRegex.lastIndex = 0;
  #|
  #|           // Handle head/body specially
  #|           if (tagName === 'head') {
  #|             current = document.head;
  #|           } else if (tagName === 'body') {
  #|             current = document.body;
  #|           } else if (tagName === 'title') {
  #|             // Title goes to head
  #|             document.head.appendChild(elem);
  #|             current = elem;
  #|           } else if (tagName === 'script' || tagName === 'style' || tagName === 'link' || tagName === 'meta') {
  #|             // These go to current context
  #|             current.appendChild(elem);
  #|             if (!attrs.includes('/>')) current = elem;
  #|           } else if (tagName !== 'html' && tagName !== '!doctype') {
  #|             current.appendChild(elem);
  #|             // Self-closing tags
  #|             const selfClosing = ['br', 'hr', 'img', 'input', 'meta', 'link'];
  #|             if (!selfClosing.includes(tagName) && !attrs.endsWith('/')) {
  #|               current = elem;
  #|             }
  #|           }
  #|         }
  #|       }
  #|
  #|       return { success: true, bodyChildren: document.body._children.length };
  #|     })
  #|   `;
  #|
  #|   try {
  #|     // First, inject the parser function
  #|     const parserResult = await vm.evalCodeAsync(parserCode, "parser.js");
  #|     if (parserResult.error) {
  #|       const error = vm.dump(parserResult.error);
  #|       parserResult.error.dispose();
  #|       return JSON.stringify({ success: false, error: "Parser error: " + JSON.stringify(error) });
  #|     }
  #|
  #|     // Get the parser function handle
  #|     const parserFn = parserResult.value;
  #|
  #|     // Create HTML string in QuickJS
  #|     const htmlHandle = vm.newString(html);
  #|
  #|     // Call the parser
  #|     const callResult = await vm.callFunction(parserFn, vm.undefined, htmlHandle);
  #|     htmlHandle.dispose();
  #|     parserFn.dispose();
  #|
  #|     if (callResult.error) {
  #|       const error = vm.dump(callResult.error);
  #|       callResult.error.dispose();
  #|       return JSON.stringify({ success: false, error: "Parse error: " + JSON.stringify(error) });
  #|     }
  #|
  #|     const result = vm.dump(callResult.value);
  #|     callResult.value.dispose();
  #|
  #|     return JSON.stringify(result);
  #|   } catch (e) {
  #|     return JSON.stringify({ success: false, error: String(e) });
  #|   }
  #| }

///|
/// FFI: Fetch URL and load into QuickJS (uses host fetch)
extern "js" fn quickjs_fetch_and_load(url : String) -> @core.Any =
  #| async (url) => {
  #|   try {
  #|     const res = await fetch(url);
  #|     if (!res.ok) {
  #|       return JSON.stringify({ success: false, status: res.status, error: "HTTP error" });
  #|     }
  #|     const html = await res.text();
  #|
  #|     // Store page URL
  #|     globalThis.__quickjs_baseUrl = new URL(url).origin;
  #|
  #|     // Load into QuickJS
  #|     const { vm } = globalThis.__quickjs_runtime;
  #|     const setUrlCode = `globalThis.__pageUrl = "${url}";`;
  #|     const urlResult = await vm.evalCodeAsync(setUrlCode, "set-url.js");
  #|     if (urlResult.value) urlResult.value.dispose();
  #|     if (urlResult.error) urlResult.error.dispose();
  #|
  #|     // Parse HTML
  #|     const loadResult = await quickjs_load_html(html);
  #|     const parsed = JSON.parse(loadResult);
  #|
  #|     return JSON.stringify({
  #|       success: parsed.success,
  #|       url,
  #|       status: res.status,
  #|       htmlLength: html.length,
  #|       ...parsed
  #|     });
  #|   } catch (e) {
  #|     return JSON.stringify({ success: false, error: String(e) });
  #|   }
  #| }

///|
/// FFI: Evaluate expression and send result via WebSocket
extern "js" fn quickjs_eval_and_send_ws(
  socket : @core.Any,
  request_id : Int,
  expression : String,
  ctx_id : String,
) -> Unit =
  #| async (socket, requestId, expression, ctxId) => {
  #|   const rt = globalThis.__quickjs_runtime;
  #|   if (!rt || !rt.vm) {
  #|     const errorResponse = JSON.stringify({
  #|       type: "error",
  #|       id: requestId,
  #|       error: "unknown error",
  #|       message: "QuickJS not initialized"
  #|     });
  #|     socket.send(errorResponse);
  #|     return;
  #|   }
  #|   const { vm } = rt;
  #|
  #|   const toBidiValue = (value, depth = 0) => {
  #|     if (depth > 10) return { type: "object" };
  #|     if (value === undefined) return { type: "undefined" };
  #|     if (value === null) return { type: "null" };
  #|     const type = typeof value;
  #|     if (type === "boolean") return { type: "boolean", value };
  #|     if (type === "number") {
  #|       if (Number.isNaN(value)) return { type: "number", value: "NaN" };
  #|       if (!Number.isFinite(value)) return { type: "number", value: value > 0 ? "Infinity" : "-Infinity" };
  #|       if (Object.is(value, -0)) return { type: "number", value: "-0" };
  #|       return { type: "number", value };
  #|     }
  #|     if (type === "string") return { type: "string", value };
  #|     if (type === "bigint") return { type: "bigint", value: String(value) };
  #|     if (type === "symbol") return { type: "symbol" };
  #|     if (type === "function") return { type: "function" };
  #|     if (Array.isArray(value)) {
  #|       return { type: "array", value: value.map(v => toBidiValue(v, depth + 1)) };
  #|     }
  #|     if (value instanceof Date) {
  #|       return { type: "date", value: value.toISOString() };
  #|     }
  #|     if (value instanceof RegExp) {
  #|       return { type: "regexp", value: { pattern: value.source, flags: value.flags } };
  #|     }
  #|     if (value instanceof Error) {
  #|       return { type: "error", value: { type: value.name, message: value.message } };
  #|     }
  #|     if (type === "object") {
  #|       try {
  #|         const entries = Object.entries(value).map(([k, v]) => [k, toBidiValue(v, depth + 1)]);
  #|         return { type: "object", value: entries };
  #|       } catch (e) {
  #|         return { type: "object" };
  #|       }
  #|     }
  #|     return { type: "undefined" };
  #|   };
  #|
  #|   // Helper to send log events
  #|   const sendLogEvent = (entry, ctxId) => {
  #|     const event = JSON.stringify({
  #|       type: "event",
  #|       method: "log.entryAdded",
  #|       params: {
  #|         type: "console",
  #|         level: entry.level,
  #|         source: { realm: "default-realm", context: ctxId },
  #|         text: entry.text,
  #|         timestamp: entry.timestamp,
  #|         method: entry.method,
  #|         args: [{ type: "string", value: entry.text }]
  #|       }
  #|     });
  #|     socket.send(event);
  #|   };
  #|
  #|   try {
  #|     const result = await vm.evalCodeAsync(expression, "eval.js");
  #|
  #|     // Collect and send console entries
  #|     try {
  #|       const logsResult = vm.evalCode("JSON.stringify(globalThis.console._logs || [])");
  #|       if (!logsResult.error) {
  #|         const logsJson = vm.dump(logsResult.value);
  #|         logsResult.value.dispose();
  #|         const logs = JSON.parse(logsJson);
  #|         for (const log of logs) {
  #|           sendLogEvent({
  #|             level: log.level === 'log' ? 'info' : log.level,
  #|             method: log.level,
  #|             text: log.args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' '),
  #|             timestamp: Date.now()
  #|           }, ctxId);
  #|         }
  #|         const clearResult = vm.evalCode("globalThis.console._logs = []");
  #|         if (!clearResult.error) clearResult.value.dispose();
  #|         else if (clearResult.error) clearResult.error.dispose();
  #|       } else {
  #|         logsResult.error.dispose();
  #|       }
  #|     } catch (e) {
  #|       // Ignore console collection errors
  #|     }
  #|
  #|     if (result.error) {
  #|       const error = vm.dump(result.error);
  #|       result.error.dispose();
  #|       const response = JSON.stringify({
  #|         type: "success",
  #|         id: requestId,
  #|         result: {
  #|           realm: "default-realm",
  #|           exceptionDetails: {
  #|             columnNumber: 0,
  #|             lineNumber: 0,
  #|             text: typeof error === "string" ? error : JSON.stringify(error),
  #|             exception: { type: "error", value: { type: error?.name || "Error" } },
  #|             stackTrace: { callFrames: [] }
  #|           }
  #|         }
  #|       });
  #|       socket.send(response);
  #|       return;
  #|     }
  #|
  #|     const value = vm.dump(result.value);
  #|     result.value.dispose();
  #|
  #|     const response = JSON.stringify({
  #|       type: "success",
  #|       id: requestId,
  #|       result: {
  #|         realm: "default-realm",
  #|         result: toBidiValue(value)
  #|       }
  #|     });
  #|     socket.send(response);
  #|   } catch (e) {
  #|     const errorResponse = JSON.stringify({
  #|       type: "error",
  #|       id: requestId,
  #|       error: "unknown error",
  #|       message: String(e)
  #|     });
  #|     socket.send(errorResponse);
  #|   }
  #| }

///|
/// Initialize QuickJS runtime
/// Returns a promise that resolves when initialization is complete
pub fn init_quickjs() -> @core.Any {
  if quickjs_state.initialized {
    return @core.any(true)
  }
  if quickjs_state.pending_init {
    // Return existing init promise
    return @core.any(true)
  }
  quickjs_state.pending_init = true
  quickjs_init_runtime()
}

///|
/// Mark QuickJS as initialized (call after awaiting init_quickjs)
pub fn mark_quickjs_initialized() -> Unit {
  quickjs_state.initialized = true
  quickjs_state.pending_init = false
}

///|
/// Check if QuickJS is initialized
pub fn is_quickjs_initialized() -> Bool {
  quickjs_state.initialized
}

///|
/// Evaluate expression using QuickJS (sync - not recommended, use async)
pub fn quickjs_eval_sync(expression : String) -> String {
  quickjs_evaluate_sync(expression)
}

///|
/// Evaluate expression using QuickJS (async)
/// Returns a promise that resolves to BiDi-formatted JSON
pub fn quickjs_eval_async(
  expression : String,
  await_promise : Bool,
) -> @core.Any {
  quickjs_evaluate_async(expression, await_promise)
}

///|
/// Evaluate expression and send result via WebSocket
pub fn quickjs_eval_ws(
  socket : @core.Any,
  request_id : Int,
  expression : String,
  ctx_id : String,
) -> Unit {
  quickjs_eval_and_send_ws(socket, request_id, expression, ctx_id)
}

///|
/// Evaluate ES module using QuickJS
/// Returns a promise that resolves to BiDi-formatted JSON
pub fn quickjs_eval_module(code : String) -> @core.Any {
  quickjs_evaluate_module(code)
}

///|
/// Load HTML into QuickJS Mock DOM
/// Returns a promise that resolves to result JSON
pub fn quickjs_load(html : String) -> @core.Any {
  quickjs_load_html(html)
}

///|
/// Fetch URL and load into QuickJS
/// Returns a promise that resolves to result JSON
pub fn quickjs_fetch(url : String) -> @core.Any {
  quickjs_fetch_and_load(url)
}