// arcrun `code` 零件 —— 沙箱核心（runtime-agnostic：Node 與 CF Workers 共用同一份）
// ---------------------------------------------------------------------------
// 語義：n8n Code node 式。config 帶一段 inline user JS，stdin 帶 input JSON。
// user code 只能：讀 `input`（已解析的 stdin JSON）、回傳一個 JSON-able 值。
// user code 碰不到：網路 / 檔案 / env / secret / Worker 物件圖。
//
// 隔離機制：user JS 跑在 QuickJS（JS 直譯器）編成的 wasm sandbox 內。QuickJS
// context 的 global 只有純 ECMAScript 內建（Object/Array/JSON/Math/Date/String…），
// 沒有 fetch / process / require / globalThis.env / WebAssembly / 任何 host binding。
// 要給的能力，只能由 host 明確、逐一注入 —— 目前唯一 curated builtin = 純函式 `sha256`，
// 且以「純 JS 演算法字串 prelude」注入（不呼叫 host、不用 async Web Crypto，Node/Worker 皆決定性）。
//
// 封裝方式：quickjs-emscripten「wasmfile」variant + 預編 WebAssembly.Module 注入（見下方 setVariant）。
// CF Workers 禁 runtime 從 bytes 編譯 wasm，故不用 singlefile(base64)；改由 build 時編好 Module。
//
// 對齊 arcrun 契約：io_model=stdin_stdout_json、no_network_syscall、no_filesystem_syscall。

// wasm 載入以「variant 注入」制：CF Workers 禁止 runtime 從 bytes 編譯 wasm
// （WebAssembly.instantiate(bytes) 被 embedder 擋），故必須用「已編好的 WebAssembly.Module」。
//   - Worker（index.ts）：import 的 .wasm 由 wrangler 綁成 WebAssembly.Module → newVariant 注入。
//   - Node/vitest：由 .wasm bytes 建 new WebAssembly.Module(...) → 同一 newVariant 路徑注入。
// 呼叫方必須在 runCode 前 setVariant()。sandbox 本身不綁定任何 variant（不 bundle 錯的 loader）。
import { newQuickJSWASMModuleFromVariant } from 'quickjs-emscripten-core';

let _variant = null;
/** 注入 quickjs variant（已含預編 WebAssembly.Module）。Worker 與 Node 各自注入自己的。 */
export function setVariant(v) { _variant = v; _modulePromise = null; }

export const DEFAULT_LIMITS = {
  timeout_ms: 1000,               // 牆鐘上限（Node/本機保護；CF 同步執行會凍結 Date.now，故非主保護）
  max_ticks: 500,                 // ★ 指令計數上限（CF 主保護）：interrupt 回呼被叫超過此數即中止。
                                  //    CF Workers 凍結同步 Date.now → 純同步無窮迴圈只能靠此計數中止。
                                  //    校準（leo21c 實測）：interrupt cadence ≈ 5000 指令/tick；
                                  //    真實 card 解析 ≈ 4 ticks；CF CPU 1102 門檻 ≈ 1000+ ticks。
                                  //    500 ticks（≈ 2.5M 指令）＝ card 的 125× 餘裕、且穩在 CF 門檻下。
                                  //    需更多算力的 user code 可提高 limits.max_ticks（但別逼近 ~1000）。
  memory_bytes: 16 * 1024 * 1024, // QuickJS runtime 記憶體硬上限
  max_stack_bytes: 512 * 1024,    // 遞迴/深堆疊上限
  max_output_bytes: 1024 * 1024,  // stdout JSON 大小上限（防跑飛）
  max_code_bytes: 256 * 1024,     // user code 本身大小上限
};

// --- curated builtin：純 JS SHA-256（UTF-8 → hex）。無 host call、無 async。 ---
// 以字串 prelude 注入沙箱，成為沙箱內一般函式 `sha256(str)`。與 Node crypto sha256 等價
// （測試 ⑤ 對 card 全文比對 content_hash 逐字相同）。
const SHA256_PRELUDE = `
function sha256(ascii) {
  function rr(n, x) { return (x >>> n) | (x << (32 - n)); }
  var mathPow = Math.pow, maxWord = mathPow(2, 32), result = '';
  var words = [], asciiBitLength;
  var utf8 = [];
  for (var ci = 0; ci < ascii.length; ci++) {
    var code = ascii.charCodeAt(ci);
    if (code < 0x80) utf8.push(code);
    else if (code < 0x800) { utf8.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f)); }
    else if (code < 0xd800 || code >= 0xe000) { utf8.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f)); }
    else {
      ci++;
      code = 0x10000 + (((code & 0x3ff) << 10) | (ascii.charCodeAt(ci) & 0x3ff));
      utf8.push(0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f));
    }
  }
  asciiBitLength = utf8.length * 8;
  var hash = sha256.h = sha256.h || [];
  var k = sha256.k = sha256.k || [];
  var primeCounter = k.length;
  var isComposite = {};
  for (var candidate = 2; primeCounter < 64; candidate++) {
    if (!isComposite[candidate]) {
      for (var i2 = 0; i2 < 313; i2 += candidate) isComposite[i2] = candidate;
      hash[primeCounter] = (mathPow(candidate, 0.5) * maxWord) | 0;
      k[primeCounter++] = (mathPow(candidate, 1 / 3) * maxWord) | 0;
    }
  }
  hash = hash.slice(0, 8);
  var bytes = utf8.slice();
  bytes.push(0x80);
  while (bytes.length % 64 - 56) bytes.push(0x00);
  for (var b = 0; b < bytes.length; b++) {
    words[b >> 2] |= bytes[b] << ((3 - b) % 4) * 8;
  }
  words[words.length] = (asciiBitLength / maxWord) | 0;
  words[words.length] = asciiBitLength;
  for (var j = 0; j < words.length;) {
    var w = words.slice(j, j += 16);
    var oldHash = hash;
    hash = hash.slice(0, 8);
    for (var i = 0; i < 64; i++) {
      var w15 = w[i - 15], w2 = w[i - 2];
      var a = hash[0], e = hash[4];
      var temp1 = hash[7]
        + (rr(6, e) ^ rr(11, e) ^ rr(25, e))
        + ((e & hash[5]) ^ ((~e) & hash[6]))
        + k[i]
        + (w[i] = (i < 16) ? w[i] : (
            w[i - 16]
            + (rr(7, w15) ^ rr(18, w15) ^ (w15 >>> 3))
            + w[i - 7]
            + (rr(17, w2) ^ rr(19, w2) ^ (w2 >>> 10))
          ) | 0);
      var temp2 = (rr(2, a) ^ rr(13, a) ^ rr(22, a))
        + ((a & hash[1]) ^ (a & hash[2]) ^ (hash[1] & hash[2]));
      hash = [(temp1 + temp2) | 0].concat(hash);
      hash[4] = (hash[4] + temp1) | 0;
    }
    for (var i = 0; i < 8; i++) hash[i] = (hash[i] + oldHash[i]) | 0;
  }
  for (var i = 0; i < 8; i++) {
    for (var j = 3; j + 1; j--) {
      var b2 = (hash[i] >> (j * 8)) & 255;
      result += ((b2 < 16) ? 0 : '') + b2.toString(16);
    }
  }
  return result;
}
`;

let _modulePromise = null;
function getModule() {
  if (!_variant) throw new Error('sandbox variant not set — 呼叫 setVariant() 注入預編 WebAssembly.Module');
  if (!_modulePromise) _modulePromise = newQuickJSWASMModuleFromVariant(_variant);
  return _modulePromise;
}

/**
 * 在沙箱內跑一段 user code。
 * @param {string} code   user 的 inline JS（函式體：可含宣告、以 `return` 回值）
 * @param {*} input       已解析的 stdin JSON（會以 `input` 綁進沙箱）
 * @param {object} [opts] { limits }
 * @returns {Promise<{success:true,data:*}|{success:false,error:string,error_type?:string}>}
 */
export async function runCode(code, input, opts = {}) {
  const limits = { ...DEFAULT_LIMITS, ...(opts.limits || {}) };

  if (typeof code !== 'string') return err('code must be a string', 'ContractError');
  if (byteLen(code) > limits.max_code_bytes) {
    return err(`code exceeds max_code_bytes (${limits.max_code_bytes})`, 'ResourceError');
  }

  const QuickJS = await getModule();
  const runtime = QuickJS.newRuntime();
  runtime.setMemoryLimit(limits.memory_bytes);
  runtime.setMaxStackSize(limits.max_stack_bytes);

  const deadline = Date.now() + limits.timeout_ms;
  let interrupted = false;
  let ticks = 0;
  runtime.setInterruptHandler(() => {
    // 主保護（CF-safe）：指令計數。CF 凍結同步 Date.now，純同步無窮迴圈只能靠此中止。
    if (++ticks > limits.max_ticks) { interrupted = true; return true; }
    // 次保護（Node/本機）：牆鐘 deadline（CF 同步期間不會推進，故僅在有 I/O 或非 CF 生效）。
    if (Date.now() > deadline) { interrupted = true; return true; }
    return false;
  });

  const ctx = runtime.newContext();
  try {
    const inputJson = JSON.stringify(input === undefined ? null : input);

    // 包裝：sha256 prelude + input 綁定 + user code 當函式體。回值 JSON.stringify 交回 host。
    const wrapped = `(() => {
      "use strict";
      ${SHA256_PRELUDE}
      const input = JSON.parse(${JSON.stringify(inputJson)});
      const __run = (input) => { ${code}
      };
      const __out = __run(input);
      return JSON.stringify(__out === undefined ? null : __out);
    })()`;

    const evalResult = ctx.evalCode(wrapped, 'user-code.js');

    if (evalResult.error) {
      const detail = ctx.dump(evalResult.error);
      evalResult.error.dispose();
      if (interrupted) return err(`execution aborted: exceeded time (${limits.timeout_ms}ms) or instruction budget (${limits.max_ticks} ticks)`, 'TimeoutError');
      const msg = typeof detail === 'object' && detail
        ? `${detail.name || 'Error'}: ${detail.message || ''}`.trim()
        : String(detail);
      return err(msg, 'UserCodeError');
    }

    const outJson = ctx.getString(evalResult.value);
    evalResult.value.dispose();

    if (byteLen(outJson) > limits.max_output_bytes) {
      return err(`output exceeds max_output_bytes (${limits.max_output_bytes})`, 'ResourceError');
    }
    return { success: true, data: JSON.parse(outJson) };
  } catch (e) {
    if (interrupted) return err(`execution aborted: exceeded time (${limits.timeout_ms}ms) or instruction budget (${limits.max_ticks} ticks)`, 'TimeoutError');
    const m = e instanceof Error ? e.message : String(e);
    if (/out of memory|memory/i.test(m)) return err('out of memory', 'ResourceError');
    return err(m, 'SandboxError');
  } finally {
    ctx.dispose();
    runtime.dispose();
  }
}

function byteLen(s) {
  if (typeof Buffer !== 'undefined') return Buffer.byteLength(s, 'utf8');
  return new TextEncoder().encode(s).length;
}

function err(message, error_type) {
  return { success: false, error: message, error_type };
}
