// v2.8.0: VM sandbox for executing workflow scripts in an isolated Node.js
// context with a restricted global scope. Uses Node's built-in `vm` module
// (zero additional dependencies).
//
// Workflow scripts can use either CommonJS (`module.exports`) or ESM syntax
// (`export const` / `export default`). ESM syntax is automatically transformed
// to CJS before execution via a lightweight regex transform.

import vm from 'node:vm';
import { readFileSync } from 'node:fs';
import type { WorkflowContext } from './types.js';

/**
 * Shared timeout for all sandboxed script execution.
 * Prevents runaway workflows from blocking the server indefinitely.
 */
const EXECUTION_TIMEOUT_MS = 30_000;

/**
 * Regex-based ESM-to-CJS transform for workflow scripts.
 *
 * Handles:
 *   - `export const|let|var <name> = <value>;` → `<name> = <value>;`
 *   - `export default <expression>;`            → `default = <expression>;`
 *   - `export default function <name>(...) {...}` → `default = function <name>(...) {...}`
 *   - `export { <name1>, <name2> }`             → removed (inline assignment)
 *
 * @param code - Raw source code (ESM or CJS).
 * @returns Code transformed to CJS assignments suitable for vm.Script.
 */
export function transformEsmToCjs(code: string): string {
  // Remove `export ` prefix from declarations and `export default` assignments.
  // Order matters: handle `export default function` before bare `export default`.
  let transformed = code
    // export default async function name(...) {...}  →  default = async function name(...) {...}
    .replace(
      /export\s+default\s+(async\s+)?function\s*\**\s*(\w+)?\s*\(/g,
      (_, asyncKw, _name) => {
        return `default = ${asyncKw ?? ''}function ${_name ?? ''}(`;
      },
    )
    // export default class Name {...}  →  default = class Name {...}
    .replace(/export\s+default\s+(class\s+\w+)/g, 'default = $1')
    // export default <expression>;  →  default = <expression>;
    .replace(/export\s+default\s+/g, 'default = ')
    // export const|let|var name = value  →  name = value
    .replace(
      /export\s+(const|let|var)\s+(\w+)\s*=/g,
      (_, _decl, name) => `${name} =`,
    )
    // export function name(...) {...}  →  (hoisted, keep as-is but remove export)
    .replace(/^export\s+(function\s+\w+)/gm, '$1')
    // export class Name {...}  →  keep but remove export
    .replace(/^export\s+(class\s+\w+)/gm, '$1')
    // export { a, b, c }  →  (remove line)
    .replace(/^export\s+\{[^}]*\}\s*;?\s*$/gm, '')
    // export { a, b as c }  →  (remove line)
    .replace(/^export\s+\{[^}]*\s+as\s+\w+[^}]*\}\s*;?\s*$/gm, '');

  return transformed;
}

/**
 * Determine whether code uses ESM export syntax (export keyword at line start
 * or after optional whitespace).
 */
export function isEsmSyntax(code: string): boolean {
  return /^\s*export\s+(const|let|var|function|class|default|\{)/m.test(code);
}

/**
 * Build a restricted sandbox object with the workflow runtime API.
 *
 * @param context - The WorkflowContext methods to expose to the script.
 * @returns A plain object suitable for vm.createContext().
 */
export function buildSandbox(context: WorkflowContext): Record<string, unknown> {
  return {
    agent: context.agent,
    parallel: context.parallel,
    pipeline: context.pipeline,
    phase: context.phase,
    log: context.log,
    budget: context.budget,
    args: context.args,
    workflow: context.workflow,

    console: {
      log: context.log,
      warn: context.log,
      error: context.log,
    },
    setTimeout,
    clearTimeout,
    setInterval: undefined,  // intentionally disabled
    clearInterval: undefined, // intentionally disabled
    Promise,
    JSON,
    Math,
    Date,
    RegExp,
    Error,
    Array,
    Object,
    String,
    Number,
    Boolean,
    Map,
    Set,
    WeakMap,
    WeakSet,
    parseInt,
    parseFloat,
    isNaN,
    isFinite,
    Symbol,
    BigInt,
    undefined,
    null: null,
    true: true,
    false: false,

    module: { exports: {} },
    exports: {},
    require: undefined, // intentionally disabled
    global: undefined,  // prevent escape via `globalThis`
  };
}

/**
 * Execute a workflow script in the sandbox and return its default export
 * (the main async function).
 *
 * @param sourceFile - Absolute path to the .js workflow file.
 * @param context    - The WorkflowContext to expose to the script.
 * @returns The workflow's default export function.
 * @throws {Error} If the script doesn't export a default async function,
 *                 or if execution fails.
 */
export function loadWorkflowScript(
  sourceFile: string,
  context: WorkflowContext,
): (...args: unknown[]) => Promise<unknown> {
  const code = readFileSync(sourceFile, 'utf8');
  const finalCode = isEsmSyntax(code) ? transformEsmToCjs(code) : code;

  const rawSandbox = buildSandbox(context);
  const sandbox = rawSandbox as Record<string, unknown> & {
    module: { exports: Record<string, unknown> };
  };

  vm.createContext(sandbox);

  try {
    const script = new vm.Script(finalCode);
    script.runInContext(sandbox, {
      timeout: EXECUTION_TIMEOUT_MS,
      filename: sourceFile,
    });
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    throw new Error(`Workflow script execution failed: ${msg}`);
  }

  // Check module.exports first (CJS), then sandbox.default (ESM transform)
  const exported = sandbox.module.exports.default ?? sandbox.default;
  // Also support `module.exports = async function(...)` (direct assignment)
  const mainFn =
    typeof sandbox.module.exports === 'function'
      ? sandbox.module.exports
      : exported;

  if (typeof mainFn !== 'function') {
    const exportedKeys = Object.keys({
      ...sandbox.module.exports,
      ...(sandbox.default ? { default: true } : {}),
    });
    throw new Error(
      `Workflow script must export a default async function. ` +
        `Found exports: ${exportedKeys.join(', ') || '(none)'}. ` +
        `Make sure your script has "export default async function main(args) {...}".`,
    );
  }

  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
  return mainFn as (...args: unknown[]) => Promise<unknown>;
}

/**
 * Load a workflow script from a source code string (rather than a file).
 * Useful for built-in workflows from the catalog that don't have a
 * corresponding .js file on disk.
 *
 * @param code       - The JavaScript source code of the workflow.
 * @param context    - The WorkflowContext to expose.
 * @param filename   - Virtual filename for stack traces (e.g. 'builtin://deep-research').
 * @returns The workflow's default export function.
 * @throws {Error} If the script doesn't export a default async function.
 */
export function loadWorkflowScriptFromCode(
  code: string,
  context: WorkflowContext,
  filename?: string,
): (...args: unknown[]) => Promise<unknown> {
  const finalCode = isEsmSyntax(code) ? transformEsmToCjs(code) : code;

  const rawSandbox = buildSandbox(context);
  const sandbox = rawSandbox as Record<string, unknown> & {
    module: { exports: Record<string, unknown> };
  };

  vm.createContext(sandbox);

  try {
    const script = new vm.Script(finalCode);
    script.runInContext(sandbox, {
      timeout: EXECUTION_TIMEOUT_MS,
      filename: filename ?? 'workflow:<anonymous>',
    });
  } catch (err) {
    const msg = err instanceof Error ? err.message : String(err);
    throw new Error(`Workflow script execution failed: ${msg}`);
  }

  const exported = sandbox.module.exports.default ?? sandbox.default;
  const mainFn =
    typeof sandbox.module.exports === 'function'
      ? sandbox.module.exports
      : exported;

  if (typeof mainFn !== 'function') {
    const exportedKeys = Object.keys({
      ...sandbox.module.exports,
      ...(sandbox.default ? { default: true } : {}),
    });
    throw new Error(
      `Workflow script must export a default async function. ` +
        `Found exports: ${exportedKeys.join(', ') || '(none)'}.`,
    );
  }

  // eslint-disable-next-line @typescript-eslint/no-unsafe-return
  return mainFn as (...args: unknown[]) => Promise<unknown>;
}

/**
 * High-level convenience: load and execute a workflow script in a single call.
 *
 * @param sourceFile - Absolute path to the .js workflow file.
 * @param context    - The WorkflowContext to expose.
 * @param args       - Optional arguments passed to the workflow function.
 * @returns The workflow's return value.
 */
export async function executeWorkflowScript(
  sourceFile: string,
  context: WorkflowContext,
  args?: Record<string, unknown>,
): Promise<unknown> {
  const mainFn = loadWorkflowScript(sourceFile, context);
  return mainFn(args);
}

/**
 * Execute a workflow from source code (string) rather than a file.
 * Convenience wrapper around `loadWorkflowScriptFromCode`.
 *
 * @param code     - The JavaScript source code of the workflow.
 * @param context  - The WorkflowContext to expose.
 * @param args     - Optional arguments passed to the workflow function.
 * @param filename - Virtual filename for stack traces.
 * @returns The workflow's return value.
 */
export async function executeWorkflowScriptFromCode(
  code: string,
  context: WorkflowContext,
  args?: Record<string, unknown>,
  filename?: string,
): Promise<unknown> {
  const mainFn = loadWorkflowScriptFromCode(code, context, filename);
  return mainFn(args);
}
