import ivm from "isolated-vm";
const MEMORY_LIMIT_MB = parseInt(process.env.SANDBOX_MEMORY_MB ?? "32", 10);
const MAX_LOG_ENTRIES = 100;
const MAX_LOG_ENTRY_LENGTH = 1e3;
function createSandbox(globals, options = {}) {
  const { timeout = 3e4 } = options;
  return {
    async execute(code) {
      const logs = [];
      const start = Date.now();
      const isolate = new ivm.Isolate({ memoryLimit: MEMORY_LIMIT_MB });
      try {
        const ctx = await isolate.createContext();
        await bootstrapConsole(ctx, logs);
        await injectGlobals(ctx, globals);
        await ctx.global.set("globalThis", void 0);
        const normalized = normalizeCode(code);
        const script = await isolate.compileScript(`(${normalized})()`);
        const result = await Promise.race([
          script.run(ctx, { promise: true, copy: true }),
          new Promise(
            (_, reject) => globalThis.setTimeout(
              () => reject(new Error(`Execution timed out after ${timeout}ms`)),
              timeout
            )
          )
        ]);
        return {
          result,
          logs,
          durationMs: Date.now() - start
        };
      } catch (error) {
        const err = error;
        return {
          result: null,
          error: err?.message ?? String(err),
          logs,
          durationMs: Date.now() - start
        };
      } finally {
        isolate.dispose();
      }
    }
  };
}
function normalizeCode(code) {
  let normalized = code.trim();
  normalized = normalized.replace(/^```(?:javascript|js|typescript|ts)?\s*\n?/i, "").replace(/\n?```\s*$/, "").trim();
  if (!/^\s*async\s*\(/.test(normalized)) {
    const isStatement = /^\s*(const|let|var|for|while|if|try|switch|return|throw|class|function)\b/.test(normalized);
    normalized = isStatement ? `async () => { ${normalized} }` : `async () => { return ${normalized} }`;
  }
  return normalized;
}
async function bootstrapConsole(ctx, logs) {
  const cb = new ivm.Callback((...args) => pushLog(logs, args), { ignored: true });
  await ctx.evalClosure(
    `globalThis.console = {
      log:   (...a) => $0(...a),
      info:  (...a) => $0(...a),
      warn:  (...a) => $0(...a),
      error: (...a) => $0(...a),
      debug: (...a) => $0(...a),
    }`,
    [cb]
  );
}
async function injectGlobals(ctx, globals) {
  const jail = ctx.global;
  for (const [key, value] of Object.entries(globals)) {
    if (value === null || value === void 0) {
      await jail.set(key, value);
      continue;
    }
    if (typeof value === "function") {
      await injectFn(ctx, value, `globalThis[${JSON.stringify(key)}]`);
      continue;
    }
    if (typeof value === "object") {
      const obj = value;
      const dataEntries = {};
      const fnProps = [];
      for (const [prop, propVal] of Object.entries(obj)) {
        if (typeof propVal === "function") {
          fnProps.push([prop, propVal]);
        } else {
          dataEntries[prop] = propVal;
        }
      }
      await jail.set(key, new ivm.ExternalCopy(dataEntries).copyInto());
      for (const [prop, fn] of fnProps) {
        await injectFn(ctx, fn, `globalThis[${JSON.stringify(key)}][${JSON.stringify(prop)}]`);
      }
      continue;
    }
    await jail.set(key, value);
  }
}
async function injectFn(ctx, fn, target) {
  const sab = new SharedArrayBuffer(4);
  const signal = new Int32Array(sab);
  const pending = { result: null };
  const startCb = new ivm.Callback(
    (...args) => {
      try {
        const ret = fn(...args);
        const p = ret instanceof Promise ? ret : Promise.resolve(ret);
        p.then(
          (v) => {
            pending.result = { ok: true, v };
            Atomics.store(signal, 0, 1);
            Atomics.notify(signal, 0);
          },
          (e) => {
            const err = e;
            pending.result = { ok: false, e: err?.message ?? String(err) };
            Atomics.store(signal, 0, 1);
            Atomics.notify(signal, 0);
          }
        );
      } catch (e) {
        const err = e;
        pending.result = { ok: false, e: err?.message ?? String(err) };
        Atomics.store(signal, 0, 1);
        Atomics.notify(signal, 0);
      }
    },
    { ignored: true }
  );
  const getResultCb = new ivm.Callback(() => {
    const r = pending.result;
    pending.result = null;
    Atomics.store(signal, 0, 0);
    return new ivm.ExternalCopy(r).copyInto();
  });
  await ctx.evalClosure(
    `const _s=$0,_sig=new Int32Array(_s),_start=$1,_get=$2
     ${target} = function(...a) {
       _start(...a)
       Atomics.wait(_sig, 0, 0)
       const r = _get()
       if (!r.ok) throw new Error(r.e)
       return r.v
     }`,
    [new ivm.ExternalCopy(sab).copyInto(), startCb, getResultCb]
  );
}
function pushLog(logs, args) {
  if (logs.length >= MAX_LOG_ENTRIES) return;
  const message = args.map((arg) => {
    if (typeof arg === "string") return arg;
    try {
      return JSON.stringify(arg);
    } catch {
      return String(arg);
    }
  }).join(" ");
  logs.push(
    message.length > MAX_LOG_ENTRY_LENGTH ? message.slice(0, MAX_LOG_ENTRY_LENGTH) + "..." : message
  );
}
export {
  createSandbox,
  normalizeCode
};
//# sourceMappingURL=sandbox.js.map
