/**
 * Build the ES module source that runs inside the dynamic Worker.
 *
 * The module exports a `WorkerEntrypoint` subclass with an `evaluate`
 * method that:
 * 1. Captures console output into `__logs`.
 * 2. Creates a recursive `tools` Proxy that dispatches calls via RPC.
 * 3. Executes the normalised user code with a `Promise.race` timeout.
 * 4. Returns `{ result, error?, logs }`.
 *
 * Tool args cross the dispatcher boundary via Workers RPC, not JSON, so
 * `Uint8Array` / `ArrayBuffer` survive intact. `Blob` / `File` aren't on
 * RPC's structured-clone allow-list, so we encode them to a tagged
 * envelope (`__encodeBinary`) and the host rehydrates them. This is
 * required for any tool whose upstream wants `multipart/form-data`
 * (OpenAI Files API, etc.).
 */
export const buildExecutorModule = (body: string, timeoutMs: number): string =>
  [
    'import { WorkerEntrypoint } from "cloudflare:workers";',
    "",
    "export default class CodeExecutor extends WorkerEntrypoint {",
    "  async evaluate(__dispatcher) {",
    "    const __logs = [];",
    '    console.log = (...a) => { __logs.push(a.map(String).join(" ")); };',
    '    console.warn = (...a) => { __logs.push("[warn] " + a.map(String).join(" ")); };',
    '    console.error = (...a) => { __logs.push("[error] " + a.map(String).join(" ")); };',
    "    const __serializeErrorValue = (value) => {",
    "      if (value instanceof Error) {",
    "        return {",
    "          __type: 'Error',",
    "          name: value.name,",
    "          message: value.message,",
    "        };",
    "      }",
    "      if (value === null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {",
    "        return value;",
    "      }",
    "      try {",
    "        return JSON.parse(JSON.stringify(value));",
    "      } catch {",
    "        return String(value);",
    "      }",
    "    };",
    "    const __renderMessage = (value) => {",
    "      if (typeof value === 'string') return value;",
    "      if (value && typeof value === 'object' && value.__type === 'Error' && typeof value.message === 'string') {",
    "        return value.message;",
    "      }",
    "      if (value instanceof Error) return value.message;",
    "      if (value && typeof value === 'object' && typeof value.message === 'string') return value.message;",
    "      if (typeof value === 'undefined') return 'Unknown error';",
    "      if (value && typeof value === 'object') {",
    "        try {",
    "          return JSON.stringify(value);",
    "        } catch {",
    "          return String(value);",
    "        }",
    "      }",
    "      return String(value);",
    "    };",
    "    const __serializeThrownError = (err) => {",
    "      const primary = __serializeErrorValue(err);",
    "      return {",
    "        kind: 'unknown',",
    "        message: __renderMessage(primary),",
    "        primary,",
    "        failures: [],",
    "        defects: [],",
    "        interrupted: false,",
    "      };",
    "    };",
    "",
    // Blob/File aren't on Workers RPC's structured-clone allow-list, so
    // we encode them to a tagged ArrayBuffer envelope before the call and
    // the host rehydrates them — and symmetrically the host encodes
    // Blob/File results which we rehydrate here. ArrayBuffer / typed
    // arrays cross natively.
    "    const __isPlainObject = (v) => {",
    "      const p = Object.getPrototypeOf(v);",
    "      return p === Object.prototype || p === null;",
    "    };",
    "    const __isBinaryEnvelope = (v) =>",
    "      v && typeof v === 'object' && v.__executorBinary === 1 &&",
    "      v.buffer instanceof ArrayBuffer && typeof v.type === 'string';",
    "    const __encodeBinary = async (value, seen = new WeakSet()) => {",
    "      if (value === null || typeof value !== 'object') return value;",
    "      if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return value;",
    "      if (seen.has(value)) throw new Error('Tool RPC payload contains a circular reference');",
    "      seen.add(value);",
    "      if (typeof File !== 'undefined' && value instanceof File) {",
    "        const out = {",
    "          __executorBinary: 1,",
    "          kind: 'file',",
    "          type: value.type,",
    "          name: value.name,",
    "          lastModified: value.lastModified,",
    "          buffer: await value.arrayBuffer(),",
    "        };",
    "        seen.delete(value);",
    "        return out;",
    "      }",
    "      if (value instanceof Blob) {",
    "        const out = { __executorBinary: 1, kind: 'blob', type: value.type, buffer: await value.arrayBuffer() };",
    "        seen.delete(value);",
    "        return out;",
    "      }",
    "      if (Array.isArray(value)) {",
    "        const out = await Promise.all(value.map((item) => __encodeBinary(item, seen)));",
    "        seen.delete(value);",
    "        return out;",
    "      }",
    "      if (!__isPlainObject(value)) { seen.delete(value); return value; }",
    "      const out = {};",
    "      for (const [k, v] of Object.entries(value)) out[k] = await __encodeBinary(v, seen);",
    "      seen.delete(value);",
    "      return out;",
    "    };",
    "    const __decodeBinary = (value, seen = new WeakSet()) => {",
    "      if (value === null || typeof value !== 'object') return value;",
    "      if (value instanceof ArrayBuffer || ArrayBuffer.isView(value)) return value;",
    "      if (seen.has(value)) throw new Error('Tool RPC payload contains a circular reference');",
    "      seen.add(value);",
    "      if (__isBinaryEnvelope(value)) {",
    "        seen.delete(value);",
    "        if (value.kind === 'file' && typeof value.name === 'string') {",
    "          return new File([value.buffer], value.name, {",
    "            type: value.type,",
    "            ...(typeof value.lastModified === 'number' ? { lastModified: value.lastModified } : {}),",
    "          });",
    "        }",
    "        return new Blob([value.buffer], { type: value.type });",
    "      }",
    "      if (Array.isArray(value)) { const out = value.map((item) => __decodeBinary(item, seen)); seen.delete(value); return out; }",
    "      if (!__isPlainObject(value)) { seen.delete(value); return value; }",
    "      const out = {};",
    "      for (const [k, v] of Object.entries(value)) out[k] = __decodeBinary(v, seen);",
    "      seen.delete(value);",
    "      return out;",
    "    };",
    "    const __publicToolErrorMessage = (error) => {",
    "      const values = [error && error.primary, ...(Array.isArray(error && error.failures) ? error.failures : [])];",
    "      for (const value of values) {",
    "        if (value && value.__type === 'Error' && value.name === 'ExecutionToolError' && typeof value.message === 'string') return value.message;",
    "      }",
    "      if (error && typeof error.message === 'string' && error.message.startsWith('Internal tool error')) return error.message;",
    "      return null;",
    "    };",
    "    const __makeToolsProxy = (path = []) => new Proxy(() => undefined, {",
    "      get(_target, prop) {",
    "        if (prop === 'then' || typeof prop === 'symbol') return undefined;",
    "        return __makeToolsProxy([...path, String(prop)]);",
    "      },",
    "      apply(_target, _thisArg, args) {",
    "        const toolPath = path.join('.');",
    "        if (!toolPath) throw new Error('Tool path missing in invocation');",
    "        return (async () => {",
    "          const encoded = await __encodeBinary(args[0]);",
    "          const data = await __dispatcher.call(toolPath, encoded);",
    "          if (!data.ok) throw new Error(__publicToolErrorMessage(data.error) || 'Internal tool error');",
    "          return __decodeBinary(data.result);",
    "        })();",
    "      },",
    "    });",
    "    const tools = __makeToolsProxy();",
    "",
    "    try {",
    "      const result = await Promise.race([",
    "        (async () => {",
    body,
    "        })(),",
    "        new Promise((_, reject) =>",
    `          setTimeout(() => reject(new Error("Execution timed out after ${timeoutMs}ms")), ${timeoutMs})`,
    "        ),",
    "      ]);",
    "      return { result, logs: __logs };",
    "    } catch (err) {",
    "      return { result: undefined, error: __serializeThrownError(err), logs: __logs };",
    "    }",
    "  }",
    "}",
  ].join("\n");
