/**
 * Minimal test worker for integration tests
 *
 * Exposes SDK methods via HTTP endpoints for E2E testing.
 * Supports both default sessions (implicit) and explicit sessions via X-Session-Id header.
 *
 * Sandbox types available:
 * - Sandbox: Base image without Python (default, lean image)
 * - SandboxPython: Full image with Python (for code interpreter tests)
 * - SandboxOpencode: Image with OpenCode CLI (for OpenCode integration tests)
 * - SandboxStandalone: Standalone binary on arbitrary base image (for binary pattern tests)
 * - SandboxMusl: Musl-based Alpine image variant (for musl binary tests)
 *
 * Use X-Sandbox-Type header to select: 'python', 'opencode', 'standalone', 'musl', or default
 */

import {
  ContainerProxy,
  getSandbox,
  proxyToSandbox,
  Sandbox
} from '@cloudflare/sandbox';
import {
  createOpencodeServer,
  proxyToOpencodeServer
} from '@cloudflare/sandbox/opencode';

import type {
  BucketDeleteResponse,
  BucketGetResponse,
  BucketPutResponse,
  BucketUnmountResponse,
  CodeContextDeleteResponse,
  ErrorResponse,
  HealthResponse,
  PortUnexposeResponse,
  SessionCreateResponse,
  SuccessResponse,
  SuccessWithMessageResponse,
  WebSocketInitResponse
} from './types';

// Export Sandbox class with different names for each container type
// The actual image is determined by the container binding in wrangler.jsonc
export { ContainerProxy };
export { Sandbox };
export { Sandbox as SandboxPython };
export { Sandbox as SandboxOpencode };
export { Sandbox as SandboxStandalone };
export { Sandbox as SandboxMusl };

interface Env {
  Sandbox: DurableObjectNamespace<Sandbox>;
  SandboxPython: DurableObjectNamespace<Sandbox>;
  SandboxOpencode: DurableObjectNamespace<Sandbox>;
  SandboxStandalone: DurableObjectNamespace<Sandbox>;
  SandboxMusl: DurableObjectNamespace<Sandbox>;
  TEST_BUCKET: R2Bucket;
  BACKUP_BUCKET: R2Bucket;
  // R2 credentials for bucket mounting tests
  CLOUDFLARE_ACCOUNT_ID?: string;
  AWS_ACCESS_KEY_ID?: string;
  AWS_SECRET_ACCESS_KEY?: string;
  // R2 credentials for backup presigned URL transfers
  R2_ACCESS_KEY_ID?: string;
  R2_SECRET_ACCESS_KEY?: string;
  BACKUP_BUCKET_NAME?: string;
  DEPLOY_HASH?: string;
}

/**
 * Interface for SandboxError shape (direct calls preserve errorResponse property).
 * Used for type-safe error handling without importing the actual class.
 */
interface SandboxErrorLike extends Error {
  errorResponse: {
    message: string;
    code?: string;
    context?: Record<string, unknown>;
    httpStatus?: number;
    suggestion?: string;
  };
  code?: string;
  context?: Record<string, unknown>;
  httpStatus?: number;
  suggestion?: string;
}

/**
 * Type guard for SandboxError-like objects.
 * Checks for the errorResponse property that direct calls preserve.
 */
function isSandboxErrorLike(error: unknown): error is SandboxErrorLike {
  return (
    error instanceof Error &&
    'errorResponse' in error &&
    typeof error.errorResponse === 'object' &&
    error.errorResponse !== null
  );
}

/**
 * Maps SandboxError subclass names to HTTP status codes and error codes.
 * Used as a fallback when errors cross the Cloudflare RPC boundary,
 * which strips own properties and prototype getters, preserving only
 * error.name and error.message.
 */
const ERROR_NAME_MAP: Record<string, { status: number; code: string }> = {
  // Backup errors
  BackupNotFoundError: { status: 404, code: 'BACKUP_NOT_FOUND' },
  BackupExpiredError: { status: 400, code: 'BACKUP_EXPIRED' },
  InvalidBackupConfigError: { status: 400, code: 'INVALID_BACKUP_CONFIG' },
  BackupCreateError: { status: 500, code: 'BACKUP_CREATE_FAILED' },
  BackupRestoreError: { status: 500, code: 'BACKUP_RESTORE_FAILED' },
  // File errors
  FileNotFoundError: { status: 404, code: 'FILE_NOT_FOUND' },
  FileExistsError: { status: 409, code: 'FILE_EXISTS' },
  FileSystemError: { status: 500, code: 'FILESYSTEM_ERROR' },
  PermissionDeniedError: { status: 403, code: 'PERMISSION_DENIED' },
  // Command errors
  CommandNotFoundError: { status: 404, code: 'COMMAND_NOT_FOUND' },
  CommandError: { status: 500, code: 'COMMAND_EXECUTION_ERROR' },
  // Process errors
  ProcessNotFoundError: { status: 404, code: 'PROCESS_NOT_FOUND' },
  ProcessError: { status: 500, code: 'PROCESS_ERROR' },
  ProcessReadyTimeoutError: { status: 408, code: 'PROCESS_READY_TIMEOUT' },
  ProcessExitedBeforeReadyError: {
    status: 500,
    code: 'PROCESS_EXITED_BEFORE_READY'
  },
  // Port errors
  PortAlreadyExposedError: { status: 409, code: 'PORT_ALREADY_EXPOSED' },
  PortNotExposedError: { status: 404, code: 'PORT_NOT_EXPOSED' },
  InvalidPortError: { status: 400, code: 'INVALID_PORT' },
  PortInUseError: { status: 409, code: 'PORT_IN_USE' },
  ServiceNotRespondingError: { status: 502, code: 'SERVICE_NOT_RESPONDING' },
  CustomDomainRequiredError: { status: 400, code: 'CUSTOM_DOMAIN_REQUIRED' },
  // Git errors
  GitRepositoryNotFoundError: {
    status: 404,
    code: 'GIT_REPOSITORY_NOT_FOUND'
  },
  GitAuthenticationError: { status: 401, code: 'GIT_AUTH_FAILED' },
  GitBranchNotFoundError: { status: 404, code: 'GIT_BRANCH_NOT_FOUND' },
  GitNetworkError: { status: 502, code: 'GIT_NETWORK_ERROR' },
  InvalidGitUrlError: { status: 400, code: 'INVALID_GIT_URL' },
  GitCloneError: { status: 500, code: 'GIT_CLONE_FAILED' },
  GitCheckoutError: { status: 500, code: 'GIT_CHECKOUT_FAILED' },
  // Code interpreter errors
  InterpreterNotReadyError: { status: 503, code: 'INTERPRETER_NOT_READY' },
  ContextNotFoundError: { status: 404, code: 'CONTEXT_NOT_FOUND' },
  CodeExecutionError: { status: 500, code: 'CODE_EXECUTION_ERROR' },
  // Session errors
  SessionAlreadyExistsError: { status: 409, code: 'SESSION_ALREADY_EXISTS' },
  SessionTerminatedError: { status: 410, code: 'SESSION_TERMINATED' },
  SessionDestroyedError: { status: 410, code: 'SESSION_DESTROYED' },
  // Port errors (generic)
  PortError: { status: 500, code: 'PORT_OPERATION_ERROR' },
  // Git errors (generic)
  GitError: { status: 500, code: 'GIT_OPERATION_FAILED' },
  // Security errors
  SandboxSecurityError: { status: 400, code: 'SECURITY_ERROR' },
  // Validation errors
  ValidationFailedError: { status: 400, code: 'VALIDATION_FAILED' },
  // Bucket errors
  BucketMountError: { status: 500, code: 'BUCKET_MOUNT_ERROR' },
  BucketUnmountError: { status: 500, code: 'BUCKET_UNMOUNT_ERROR' },
  S3FSMountError: { status: 500, code: 'S3FS_MOUNT_ERROR' },
  InvalidMountConfigError: { status: 400, code: 'INVALID_MOUNT_CONFIG' },
  MissingCredentialsError: { status: 400, code: 'MISSING_CREDENTIALS' }
};

async function parseBody(request: Request): Promise<any> {
  try {
    return await request.json();
  } catch {
    return {};
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // Route requests to exposed container ports via their preview URLs
    const proxyResponse = await proxyToSandbox(request, env);
    if (proxyResponse) return proxyResponse;

    const url = new URL(request.url);
    // Skip JSON body parsing for streaming endpoints to preserve request.body
    const isStreamingUpload =
      url.pathname === '/api/file/write-stream' && request.method === 'PUT';
    const body = isStreamingUpload ? {} : await parseBody(request);

    // Get sandbox ID from header or query param (WebSocket can't send headers)
    // Sandbox ID determines which container instance (Durable Object)
    const baseSandboxId =
      request.headers.get('X-Sandbox-Id') ||
      url.searchParams.get('sandboxId') ||
      'default-test-sandbox';

    const transportHeader = request.headers.get('X-Sandbox-Transport');
    const transport: 'http' | 'websocket' | 'rpc' =
      transportHeader === 'websocket' || transportHeader === 'rpc'
        ? transportHeader
        : 'http';

    // Suffix sandbox ID with transport so each transport gets its own DO instance
    const sandboxId = `${baseSandboxId}-${transport}`;

    // Check if keepAlive is requested
    const keepAliveHeader = request.headers.get('X-Sandbox-KeepAlive');
    const keepAlive = keepAliveHeader === 'true';
    const sleepAfter = request.headers.get('X-Sandbox-Sleep-After');
    const enableDefaultSessionHeader = request.headers.get(
      'X-Sandbox-Enable-Default-Session'
    );
    const enableDefaultSession =
      enableDefaultSessionHeader === 'false' ? false : undefined;

    // Select sandbox type based on X-Sandbox-Type header
    const sandboxType = request.headers.get('X-Sandbox-Type');
    let sandboxNamespace: DurableObjectNamespace<Sandbox>;
    if (sandboxType === 'python') {
      sandboxNamespace = env.SandboxPython;
    } else if (sandboxType === 'opencode') {
      sandboxNamespace = env.SandboxOpencode;
    } else if (sandboxType === 'standalone') {
      sandboxNamespace = env.SandboxStandalone;
    } else if (sandboxType === 'musl') {
      sandboxNamespace = env.SandboxMusl;
    } else {
      sandboxNamespace = env.Sandbox;
    }

    const sandbox = getSandbox(sandboxNamespace, sandboxId, {
      keepAlive,
      transport,
      ...(sleepAfter !== null && { sleepAfter }),
      ...(enableDefaultSession !== undefined && { enableDefaultSession })
    });

    // Get session ID from header (optional)
    // If provided, retrieve the session fresh from the Sandbox DO on each request
    const sessionId = request.headers.get('X-Session-Id');

    // Executor pattern: retrieve session fresh if specified, otherwise use sandbox
    // Important: We get the session fresh on EVERY request to respect RPC lifecycle
    // The ExecutionSession stub is only valid during this request's execution context
    const executor = sessionId ? await sandbox.getSession(sessionId) : sandbox;

    try {
      // WebSocket init endpoint - starts all WebSocket servers
      if (url.pathname === '/api/init' && request.method === 'POST') {
        const processes = await sandbox.listProcesses();
        const isServerRunning = (commandFragment: string): boolean =>
          processes.some(
            (p) => p.status === 'running' && p.command.includes(commandFragment)
          );

        const serversToStart: Array<{
          name: string;
          port: number;
          start: Promise<Process>;
        }> = [];

        // Echo server
        if (!isServerRunning('/tmp/ws-echo.ts')) {
          const echoScript = `
const port = 8080;
Bun.serve({
  port,
  fetch(req, server) {
    if (server.upgrade(req)) return;
    return new Response('Expected WebSocket', { status: 400 });
  },
  websocket: {
    message(ws, message) { ws.send(message); },
    open(ws) { console.log('Echo client connected'); },
    close(ws) { console.log('Echo client disconnected'); },
  },
});
console.log('Echo server on port ' + port);
`;
          await sandbox.writeFile('/tmp/ws-echo.ts', echoScript);
          serversToStart.push({
            name: 'echo',
            port: 8080,
            start: sandbox.startProcess('bun run /tmp/ws-echo.ts')
          });
        }

        // Python code server
        if (!isServerRunning('/tmp/ws-code.ts')) {
          const codeScript = `
const port = 8081;
Bun.serve({
  port,
  fetch(req, server) {
    if (server.upgrade(req)) return;
    return new Response('Expected WebSocket', { status: 400 });
  },
  websocket: {
    async message(ws, message) {
      try {
        const data = JSON.parse(message.toString());
        if (data.type === 'execute') {
          ws.send(JSON.stringify({ type: 'executing', timestamp: Date.now() }));
          const filename = '/tmp/code_' + Date.now() + '.py';
          await Bun.write(filename, data.code);
          const proc = Bun.spawn(['python3', filename], { stdout: 'pipe', stderr: 'pipe' });
          const reader = proc.stdout.getReader();
          const decoder = new TextDecoder();
          (async () => {
            try {
              while (true) {
                const { done, value } = await reader.read();
                if (done) break;
                const text = decoder.decode(value, { stream: true });
                if (text) ws.send(JSON.stringify({ type: 'stdout', data: text, timestamp: Date.now() }));
              }
            } catch (e) {}
          })();
          const stderrReader = proc.stderr.getReader();
          (async () => {
            try {
              while (true) {
                const { done, value } = await stderrReader.read();
                if (done) break;
                const text = decoder.decode(value, { stream: true });
                if (text) ws.send(JSON.stringify({ type: 'stderr', data: text, timestamp: Date.now() }));
              }
            } catch (e) {}
          })();
          const exitCode = await proc.exited;
          ws.send(JSON.stringify({ type: 'completed', exitCode, timestamp: Date.now() }));
          try { await Bun.spawn(['rm', '-f', filename]).exited; } catch (e) {}
        }
      } catch (error) {
        ws.send(JSON.stringify({ type: 'error', message: error.message, timestamp: Date.now() }));
      }
    },
    open(ws) { ws.send(JSON.stringify({ type: 'ready', message: 'Code server ready', timestamp: Date.now() })); },
  },
});
console.log('Code server on port ' + port);
`;
          await sandbox.writeFile('/tmp/ws-code.ts', codeScript);
          serversToStart.push({
            name: 'code',
            port: 8081,
            start: sandbox.startProcess('bun run /tmp/ws-code.ts')
          });
        }

        // Terminal server
        if (!isServerRunning('/tmp/ws-terminal.ts')) {
          const terminalScript = `
const port = 8082;
Bun.serve({
  port,
  fetch(req, server) {
    if (server.upgrade(req)) return;
    return new Response('Expected WebSocket', { status: 400 });
  },
  websocket: {
    async message(ws, message) {
      try {
        const data = JSON.parse(message.toString());
        if (data.type === 'command') {
          ws.send(JSON.stringify({ type: 'executing', command: data.command, timestamp: Date.now() }));
          const proc = Bun.spawn(['sh', '-c', data.command], { stdout: 'pipe', stderr: 'pipe' });
          const stdout = await new Response(proc.stdout).text();
          const stderr = await new Response(proc.stderr).text();
          const exitCode = await proc.exited;
          ws.send(JSON.stringify({ type: 'result', stdout, stderr, exitCode, timestamp: Date.now() }));
        }
      } catch (error) {
        ws.send(JSON.stringify({ type: 'error', message: error.message, timestamp: Date.now() }));
      }
    },
    open(ws) { ws.send(JSON.stringify({ type: 'ready', message: 'Terminal ready', cwd: process.cwd(), timestamp: Date.now() })); },
  },
});
console.log('Terminal server on port ' + port);
`;
          await sandbox.writeFile('/tmp/ws-terminal.ts', terminalScript);
          serversToStart.push({
            name: 'terminal',
            port: 8082,
            start: sandbox.startProcess('bun run /tmp/ws-terminal.ts')
          });
        }

        // Start all servers and wait until their target ports are accepting connections.
        const results = await Promise.allSettled(
          serversToStart.map(async (server) => {
            const process = await server.start;
            await process.waitForPort(server.port, {
              mode: 'tcp',
              timeout: 30000,
              interval: 250
            });
            return server.name;
          })
        );

        const failedCount = results.filter(
          (r) => r.status === 'rejected'
        ).length;
        const succeededCount = results.filter(
          (r) => r.status === 'fulfilled'
        ).length;

        const response: WebSocketInitResponse = {
          success: failedCount === 0,
          serversStarted: succeededCount,
          serversFailed: failedCount,
          errors:
            failedCount > 0
              ? results
                  .filter((r) => r.status === 'rejected')
                  .map(
                    (r) =>
                      (r as PromiseRejectedResult).reason?.message ||
                      String((r as PromiseRejectedResult).reason)
                  )
              : undefined
        };
        return new Response(JSON.stringify(response), {
          headers: { 'Content-Type': 'application/json' },
          status: failedCount > 0 ? 500 : 200
        });
      }

      // WebSocket endpoints
      const upgradeHeader = request.headers.get('Upgrade');
      if (upgradeHeader?.toLowerCase() === 'websocket') {
        if (url.pathname === '/ws/echo') {
          return await sandbox.wsConnect(request, 8080);
        }
        if (url.pathname === '/ws/code') {
          return await sandbox.wsConnect(request, 8081);
        }
        if (url.pathname === '/ws/terminal') {
          return await sandbox.wsConnect(request, 8082);
        }
      }

      // Health check
      if (url.pathname === '/health') {
        const response: HealthResponse = {
          status: 'ok',
          deploy_hash: env.DEPLOY_HASH
        };
        return new Response(JSON.stringify(response), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // OpenCode direct server proxy helper
      if (
        url.pathname === '/api/opencode/proxy-server/global-health' &&
        request.method === 'GET'
      ) {
        let server:
          | Awaited<ReturnType<typeof createOpencodeServer>>
          | undefined;

        try {
          server = await createOpencodeServer(sandbox, {
            port: 4096
          });

          const opencodeRequest = new Request(
            `${url.origin}/global/health${url.search}`,
            request
          );

          const response = await proxyToOpencodeServer(
            opencodeRequest,
            sandbox,
            server
          );
          const body = await response.arrayBuffer();

          return new Response(body, {
            status: response.status,
            headers: response.headers
          });
        } finally {
          if (server) {
            await server.close();
          }
        }
      }

      // Session management
      if (url.pathname === '/api/session/create' && request.method === 'POST') {
        const session = await sandbox.createSession(body);
        // Note: We don't store the session - it will be retrieved fresh via getSession() on each request
        const response: SessionCreateResponse = {
          success: true,
          sessionId: session.id
        };
        return new Response(JSON.stringify(response), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      if (url.pathname === '/api/session/delete' && request.method === 'POST') {
        const result = await sandbox.deleteSession(body.sessionId);
        return new Response(JSON.stringify(result), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      if (url.pathname === '/api/state' && request.method === 'GET') {
        const result = await sandbox.getState();
        return new Response(JSON.stringify(result), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      if (url.pathname === '/api/placement-id' && request.method === 'GET') {
        const placementId = await sandbox.getContainerPlacementId();
        return new Response(JSON.stringify({ placementId }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Command execution
      if (url.pathname === '/api/execute' && request.method === 'POST') {
        const result = await executor.exec(body.command, {
          env: body.env,
          cwd: body.cwd,
          timeout: body.timeout
        });
        return new Response(JSON.stringify(result), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Command execution with streaming
      if (url.pathname === '/api/execStream' && request.method === 'POST') {
        console.log(
          '[TestWorker] execStream called for command:',
          body.command
        );
        const startTime = Date.now();
        const stream = await executor.execStream(body.command, {
          env: body.env,
          cwd: body.cwd,
          timeout: body.timeout
        });
        console.log(
          '[TestWorker] Stream received in',
          Date.now() - startTime,
          'ms'
        );

        // Return SSE stream directly
        return new Response(stream, {
          headers: {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            Connection: 'keep-alive'
          }
        });
      }

      // Git clone
      if (url.pathname === '/api/git/clone' && request.method === 'POST') {
        const result = await executor.gitCheckout(body.repoUrl, {
          branch: body.branch,
          targetDir: body.targetDir,
          depth: body.depth,
          cloneTimeoutMs: body.cloneTimeoutMs
        });
        return new Response(JSON.stringify(result), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Bucket mount
      if (url.pathname === '/api/bucket/mount' && request.method === 'POST') {
        await sandbox.mountBucket(body.bucket, body.mountPath, body.options);
        const response: SuccessResponse = { success: true };
        return new Response(JSON.stringify(response), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Bucket unmount
      if (url.pathname === '/api/bucket/unmount' && request.method === 'POST') {
        await sandbox.unmountBucket(body.mountPath);
        const response: BucketUnmountResponse = { success: true };
        return new Response(JSON.stringify(response), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // R2 bucket put
      if (url.pathname === '/api/bucket/put' && request.method === 'POST') {
        await env.TEST_BUCKET.put(body.key, body.content, {
          httpMetadata: body.contentType
            ? { contentType: body.contentType }
            : undefined
        });
        const response: BucketPutResponse = { success: true, key: body.key };
        return new Response(JSON.stringify(response), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // R2 bucket get
      if (url.pathname === '/api/bucket/get' && request.method === 'GET') {
        const key = url.searchParams.get('key');
        if (!key) {
          const errorResponse: ErrorResponse = {
            error: 'Key parameter required'
          };
          return new Response(JSON.stringify(errorResponse), {
            status: 400,
            headers: { 'Content-Type': 'application/json' }
          });
        }
        const object = await env.TEST_BUCKET.get(key);
        if (!object) {
          const errorResponse: ErrorResponse = { error: 'Object not found' };
          return new Response(JSON.stringify(errorResponse), {
            status: 404,
            headers: { 'Content-Type': 'application/json' }
          });
        }
        const response: BucketGetResponse = {
          success: true,
          key,
          content: await object.text(),
          contentType: object.httpMetadata?.contentType,
          size: object.size
        };
        return new Response(JSON.stringify(response), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // R2 bucket delete
      if (url.pathname === '/api/bucket/delete' && request.method === 'POST') {
        await env.TEST_BUCKET.delete(body.key);
        const response: BucketDeleteResponse = { success: true, key: body.key };
        return new Response(JSON.stringify(response), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // File read
      if (url.pathname === '/api/file/read' && request.method === 'POST') {
        const file = await executor.readFile(body.path);
        return new Response(JSON.stringify(file), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // File read stream
      if (url.pathname === '/api/read/stream' && request.method === 'POST') {
        const stream = await executor.readFileStream(body.path);
        return new Response(stream, {
          headers: {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            Connection: 'keep-alive'
          }
        });
      }

      // File write
      if (url.pathname === '/api/file/write' && request.method === 'POST') {
        await executor.writeFile(body.path, body.content);
        return new Response(JSON.stringify({ success: true }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // File write via streaming (request body piped directly as a ReadableStream)
      if (
        url.pathname === '/api/file/write-stream' &&
        request.method === 'PUT'
      ) {
        const filePath = request.headers.get('X-File-Path');
        if (!filePath || !request.body) {
          return new Response(
            JSON.stringify({ error: 'X-File-Path header and body required' }),
            { status: 400, headers: { 'Content-Type': 'application/json' } }
          );
        }
        const result = await executor.writeFile(filePath, request.body);
        return new Response(JSON.stringify(result), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // File read-binary: readFile({ encoding: 'none' }) — rpc transport only.
      // Returns raw binary response so the test can sha256 the collected bytes.
      if (
        url.pathname === '/api/file/read-binary' &&
        request.method === 'POST'
      ) {
        const result = await executor.readFile(body.path, { encoding: 'none' });
        return new Response(result.content, {
          headers: { 'Content-Type': result.mimeType }
        });
      }

      // File mkdir
      if (url.pathname === '/api/file/mkdir' && request.method === 'POST') {
        await executor.mkdir(body.path, { recursive: body.recursive });
        return new Response(JSON.stringify({ success: true }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // File delete
      if (url.pathname === '/api/file/delete' && request.method === 'DELETE') {
        await executor.deleteFile(body.path);
        return new Response(JSON.stringify({ success: true }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // File rename
      if (url.pathname === '/api/file/rename' && request.method === 'POST') {
        await executor.renameFile(body.oldPath, body.newPath);
        return new Response(JSON.stringify({ success: true }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // File move
      if (url.pathname === '/api/file/move' && request.method === 'POST') {
        await executor.moveFile(body.sourcePath, body.destinationPath);
        return new Response(JSON.stringify({ success: true }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // List files
      if (url.pathname === '/api/list-files' && request.method === 'POST') {
        const result = await executor.listFiles(body.path, body.options);
        return new Response(JSON.stringify(result), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // File exists
      if (url.pathname === '/api/file/exists' && request.method === 'POST') {
        const result = await executor.exists(body.path);
        return new Response(JSON.stringify(result), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Process start
      if (url.pathname === '/api/process/start' && request.method === 'POST') {
        const process = await executor.startProcess(body.command, {
          processId: body.processId
        });
        return new Response(JSON.stringify(process), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Process waitForLog - waits for a log pattern
      if (
        url.pathname.startsWith('/api/process/') &&
        url.pathname.endsWith('/waitForLog') &&
        request.method === 'POST'
      ) {
        const pathParts = url.pathname.split('/');
        const processId = pathParts[3];
        const process = await executor.getProcess(processId);
        if (!process) {
          return new Response(JSON.stringify({ error: 'Process not found' }), {
            status: 404,
            headers: { 'Content-Type': 'application/json' }
          });
        }
        // pattern can be string or regex pattern (as string starting with /)
        let pattern = body.pattern;
        if (
          typeof pattern === 'string' &&
          pattern.startsWith('/') &&
          pattern.endsWith('/')
        ) {
          // Convert regex string to RegExp
          pattern = new RegExp(pattern.slice(1, -1));
        }
        const result = await process.waitForLog(pattern, body.timeout);
        return new Response(JSON.stringify(result), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Process waitForPort - waits for a port to be available
      if (
        url.pathname.startsWith('/api/process/') &&
        url.pathname.endsWith('/waitForPort') &&
        request.method === 'POST'
      ) {
        const pathParts = url.pathname.split('/');
        const processId = pathParts[3];
        const process = await executor.getProcess(processId);
        if (!process) {
          return new Response(JSON.stringify({ error: 'Process not found' }), {
            status: 404,
            headers: { 'Content-Type': 'application/json' }
          });
        }
        // Build WaitForPortOptions from request body.
        // Accept both flat fields and nested `options` payloads.
        const waitOptions =
          body.options && typeof body.options === 'object'
            ? body.options
            : body;
        await process.waitForPort(body.port, {
          mode: waitOptions.mode,
          path: waitOptions.path,
          status: waitOptions.status,
          timeout: waitOptions.timeout,
          interval: waitOptions.interval
        });
        return new Response(JSON.stringify({ success: true }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Process waitForExit - waits for process to exit
      if (
        url.pathname.startsWith('/api/process/') &&
        url.pathname.endsWith('/waitForExit') &&
        request.method === 'POST'
      ) {
        const pathParts = url.pathname.split('/');
        const processId = pathParts[3];
        const process = await executor.getProcess(processId);
        if (!process) {
          return new Response(JSON.stringify({ error: 'Process not found' }), {
            status: 404,
            headers: { 'Content-Type': 'application/json' }
          });
        }
        const result = await process.waitForExit(body.timeout);
        return new Response(JSON.stringify(result), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Process list
      if (url.pathname === '/api/process/list' && request.method === 'GET') {
        const processes = await executor.listProcesses();
        return new Response(JSON.stringify(processes), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Process get by ID
      if (
        url.pathname.startsWith('/api/process/') &&
        request.method === 'GET'
      ) {
        const pathParts = url.pathname.split('/');
        const processId = pathParts[3];

        // Handle /api/process/:id/logs
        if (pathParts[4] === 'logs') {
          const logs = await executor.getProcessLogs(processId);
          return new Response(JSON.stringify(logs), {
            headers: { 'Content-Type': 'application/json' }
          });
        }

        // Handle /api/process/:id/stream (SSE)
        if (pathParts[4] === 'stream') {
          const stream = await executor.streamProcessLogs(processId);

          // Convert AsyncIterable to ReadableStream for SSE
          const readableStream = new ReadableStream({
            async start(controller) {
              try {
                for await (const event of stream) {
                  const sseData = `data: ${JSON.stringify(event)}\n\n`;
                  controller.enqueue(new TextEncoder().encode(sseData));
                }
                controller.close();
              } catch (error) {
                controller.error(error);
              }
            }
          });

          return new Response(readableStream, {
            headers: {
              'Content-Type': 'text/event-stream',
              'Cache-Control': 'no-cache',
              Connection: 'keep-alive'
            }
          });
        }

        // Handle /api/process/:id (get single process)
        if (!pathParts[4]) {
          const process = await executor.getProcess(processId);
          return new Response(JSON.stringify(process), {
            headers: { 'Content-Type': 'application/json' }
          });
        }
      }

      // Process kill by ID
      if (
        url.pathname.startsWith('/api/process/') &&
        request.method === 'DELETE'
      ) {
        const processId = url.pathname.split('/')[3];
        await executor.killProcess(processId);
        return new Response(JSON.stringify({ success: true }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Kill all processes
      if (
        url.pathname === '/api/process/kill-all' &&
        request.method === 'POST'
      ) {
        await executor.killAllProcesses();
        return new Response(JSON.stringify({ success: true }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Port exposure (ONLY works with sandbox - sessions don't expose ports)
      if (url.pathname === '/api/port/expose' && request.method === 'POST') {
        if (sessionId) {
          return new Response(
            JSON.stringify({
              error:
                'Port exposure not supported for explicit sessions. Use default sandbox.'
            }),
            {
              status: 400,
              headers: { 'Content-Type': 'application/json' }
            }
          );
        }
        const hostname = url.hostname + (url.port ? `:${url.port}` : '');
        const preview = await sandbox.exposePort(body.port, {
          name: body.name,
          hostname: hostname,
          token: body.token
        });
        return new Response(JSON.stringify(preview), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Port exposure status (ONLY works with sandbox - sessions don't expose ports)
      if (url.pathname === '/api/exposed-ports' && request.method === 'GET') {
        if (sessionId) {
          return new Response(
            JSON.stringify({
              error:
                'Port exposure not supported for explicit sessions. Use default sandbox.'
            }),
            {
              status: 400,
              headers: { 'Content-Type': 'application/json' }
            }
          );
        }
        const hostname = url.hostname + (url.port ? `:${url.port}` : '');
        const ports = await sandbox.getExposedPorts(hostname);
        return new Response(JSON.stringify(ports), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      if (
        url.pathname.startsWith('/api/exposed-ports/') &&
        request.method === 'GET'
      ) {
        if (sessionId) {
          return new Response(
            JSON.stringify({
              error:
                'Port exposure not supported for explicit sessions. Use default sandbox.'
            }),
            {
              status: 400,
              headers: { 'Content-Type': 'application/json' }
            }
          );
        }
        const pathParts = url.pathname.split('/');
        const port = parseInt(pathParts[3], 10);
        if (!Number.isNaN(port)) {
          const exposed = await sandbox.isPortExposed(port);
          return new Response(JSON.stringify({ exposed, port }), {
            headers: { 'Content-Type': 'application/json' }
          });
        }
      }

      // Port unexpose (ONLY works with sandbox - sessions don't expose ports)
      if (
        url.pathname.startsWith('/api/exposed-ports/') &&
        request.method === 'DELETE'
      ) {
        if (sessionId) {
          return new Response(
            JSON.stringify({
              error:
                'Port exposure not supported for explicit sessions. Use default sandbox.'
            }),
            {
              status: 400,
              headers: { 'Content-Type': 'application/json' }
            }
          );
        }
        const pathParts = url.pathname.split('/');
        const port = parseInt(pathParts[3], 10);
        if (!Number.isNaN(port)) {
          await sandbox.unexposePort(port);
          return new Response(JSON.stringify({ success: true, port }), {
            headers: { 'Content-Type': 'application/json' }
          });
        }
      }

      // Tunnels (RPC-only)
      if (url.pathname === '/api/tunnel/get' && request.method === 'POST') {
        if (transport !== 'rpc') {
          return new Response(
            JSON.stringify({ error: 'Tunnels require transport=rpc' }),
            { status: 400, headers: { 'Content-Type': 'application/json' } }
          );
        }
        const info = await sandbox.tunnels.get(body.port, body.options);
        return new Response(JSON.stringify(info), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      if (url.pathname === '/api/tunnel/list' && request.method === 'GET') {
        if (transport !== 'rpc') {
          return new Response(
            JSON.stringify({ error: 'Tunnels require transport=rpc' }),
            { status: 400, headers: { 'Content-Type': 'application/json' } }
          );
        }
        const tunnels = await sandbox.tunnels.list();
        return new Response(JSON.stringify({ tunnels }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      if (
        url.pathname.startsWith('/api/tunnel/') &&
        request.method === 'DELETE'
      ) {
        if (transport !== 'rpc') {
          return new Response(
            JSON.stringify({ error: 'Tunnels require transport=rpc' }),
            { status: 400, headers: { 'Content-Type': 'application/json' } }
          );
        }
        const portStr = url.pathname.slice('/api/tunnel/'.length);
        const port = Number.parseInt(portStr, 10);
        if (!Number.isFinite(port)) {
          return new Response(
            JSON.stringify({ error: `Invalid port: ${portStr}` }),
            { status: 400, headers: { 'Content-Type': 'application/json' } }
          );
        }
        await sandbox.tunnels.destroy(port);
        return new Response(JSON.stringify({ success: true, port }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Environment variables
      if (url.pathname === '/api/env/set' && request.method === 'POST') {
        await executor.setEnvVars(body.envVars);
        return new Response(JSON.stringify({ success: true }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Code Interpreter - Create Context
      if (
        url.pathname === '/api/code/context/create' &&
        request.method === 'POST'
      ) {
        const context = await executor.createCodeContext(body);
        return new Response(JSON.stringify(context), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Code Interpreter - List Contexts
      if (
        url.pathname === '/api/code/context/list' &&
        request.method === 'GET'
      ) {
        const contexts = await executor.listCodeContexts();
        return new Response(JSON.stringify(contexts), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Code Interpreter - Delete Context
      if (
        url.pathname.startsWith('/api/code/context/') &&
        request.method === 'DELETE'
      ) {
        const pathParts = url.pathname.split('/');
        const contextId = pathParts[4]; // /api/code/context/:id
        await executor.deleteCodeContext(contextId);
        return new Response(JSON.stringify({ success: true, contextId }), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Code Interpreter - Execute Code
      if (url.pathname === '/api/code/execute' && request.method === 'POST') {
        const execution = await executor.runCode(body.code, body.options || {});
        return new Response(JSON.stringify(execution), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Code Interpreter - Execute Code with Streaming
      if (
        url.pathname === '/api/code/execute/stream' &&
        request.method === 'POST'
      ) {
        const stream = await executor.runCodeStream(
          body.code,
          body.options || {}
        );
        return new Response(stream, {
          headers: {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            Connection: 'keep-alive'
          }
        });
      }

      // Backup - Create backup
      if (url.pathname === '/api/backup/create' && request.method === 'POST') {
        const backup = await sandbox.createBackup(body);
        return new Response(JSON.stringify(backup), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Backup - Restore backup
      if (url.pathname === '/api/backup/restore' && request.method === 'POST') {
        const result = await sandbox.restoreBackup(body);
        return new Response(JSON.stringify(result), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // File Watch - Stream events via public Sandbox API.
      if (url.pathname === '/api/watch' && request.method === 'POST') {
        const stream = await sandbox.watch(body.path, {
          recursive: body.recursive,
          include: body.include,
          exclude: body.exclude,
          sessionId: sessionId ?? undefined
        });

        return new Response(stream, {
          headers: {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            Connection: 'keep-alive'
          }
        });
      }

      // File Watch - Check retained change state via public Sandbox API.
      if (url.pathname === '/api/watch/check' && request.method === 'POST') {
        const result = await sandbox.checkChanges(body.path, {
          recursive: body.recursive,
          include: body.include,
          exclude: body.exclude,
          since: body.since,
          sessionId: sessionId ?? undefined
        });

        return new Response(JSON.stringify(result), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Cleanup endpoint - destroys the sandbox container
      // This is used by E2E tests to explicitly clean up after each test
      if (url.pathname === '/cleanup' && request.method === 'POST') {
        await sandbox.destroy();
        const response: SuccessWithMessageResponse = {
          success: true,
          message: 'Sandbox destroyed'
        };
        return new Response(JSON.stringify(response), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // Test-only: SIGTERM the container without destroying the DO, so tests
      // can exercise restart-recovery paths (port re-exposure, token
      // persistence). The next RPC on the sandbox will start a fresh
      // container, which triggers onStart().
      if (url.pathname === '/api/container/stop' && request.method === 'POST') {
        await (sandbox as unknown as { stop: () => Promise<void> }).stop();
        const response: SuccessWithMessageResponse = {
          success: true,
          message: 'Container stopped'
        };
        return new Response(JSON.stringify(response), {
          headers: { 'Content-Type': 'application/json' }
        });
      }

      // PTY: Browser test page for Playwright tests
      if (url.pathname === '/terminal-test') {
        const sessionId =
          url.searchParams.get('sessionId') || `browser-test-${Date.now()}`;
        return new Response(getTerminalTestPage(sandboxId, sessionId), {
          headers: { 'Content-Type': 'text/html' }
        });
      }

      // PTY: WebSocket terminal proxy
      if (
        url.pathname === '/terminal' ||
        url.pathname.startsWith('/terminal/')
      ) {
        const upgradeHeader = request.headers.get('Upgrade');
        if (upgradeHeader?.toLowerCase() !== 'websocket') {
          return new Response('WebSocket upgrade required', { status: 426 });
        }

        const pathParts = url.pathname.split('/').filter(Boolean);

        if (pathParts.length === 1) {
          return sandbox.terminal(request, {
            cols: parseInt(url.searchParams.get('cols') || '80', 10),
            rows: parseInt(url.searchParams.get('rows') || '24', 10)
          });
        } else {
          const ptySessionId = pathParts[1];
          const session = await sandbox.getSession(ptySessionId);
          return session.terminal(request, {
            cols: parseInt(url.searchParams.get('cols') || '80', 10),
            rows: parseInt(url.searchParams.get('rows') || '24', 10)
          });
        }
      }

      return new Response('Not found', { status: 404 });
    } catch (error) {
      // Handle SandboxError with proper code and httpStatus.
      //
      // Two paths exist:
      // 1. Direct calls (error has `errorResponse` own property)
      // 2. RPC calls (only `error.name` and `error.message` survive the
      //    Cloudflare RPC boundary — own properties are stripped)
      //
      // We try (1) first, then fall back to (2) using error.name mapping.
      if (isSandboxErrorLike(error)) {
        return new Response(
          JSON.stringify({
            error: error.message,
            code: error.code,
            context: error.context,
            suggestion: error.suggestion
          }),
          {
            status: error.httpStatus ?? 500,
            headers: { 'Content-Type': 'application/json' }
          }
        );
      }

      // RPC fallback: derive HTTP status and error code from error.name
      // Cloudflare RPC strips custom error classes, converting them to generic Error
      // but preserves the class name in the message as "ClassName: actual message"
      if (error instanceof Error) {
        let errorName = error.name;
        let errorMessage = error.message;

        // Try to extract original error class from message format "ClassName: message"
        if (errorName === 'Error' && error.message.includes(': ')) {
          const colonIndex = error.message.indexOf(': ');
          const potentialClassName = error.message.slice(0, colonIndex);
          // Only use it if it looks like an error class name (PascalCase ending in Error)
          if (/^[A-Z][a-zA-Z]*Error$/.test(potentialClassName)) {
            errorName = potentialClassName;
            errorMessage = error.message.slice(colonIndex + 2);
          }
        }

        const mapping = ERROR_NAME_MAP[errorName];
        if (mapping) {
          return new Response(
            JSON.stringify({
              error: errorMessage,
              code: mapping.code
            }),
            {
              status: mapping.status,
              headers: { 'Content-Type': 'application/json' }
            }
          );
        }
      }

      return new Response(
        JSON.stringify({
          error: error instanceof Error ? error.message : 'Unknown error'
        }),
        {
          status: 500,
          headers: { 'Content-Type': 'application/json' }
        }
      );
    }
  }
};

function getTerminalTestPage(sandboxId: string, sessionId: string): string {
  return `<!DOCTYPE html>
<html>
<head>
  <title>Terminal Test</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.css">
  <style>
    body { margin: 0; padding: 20px; background: #1e1e1e; }
    #terminal { width: 100%; height: 400px; }
    #status { color: white; margin-bottom: 10px; font-family: monospace; }
  </style>
</head>
<body>
  <div id="status" data-testid="connection-status">disconnected</div>
  <div id="terminal" data-testid="terminal-container"></div>

  <script type="module">
    import { Terminal } from 'https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/+esm';
    import { FitAddon } from 'https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.10.0/+esm';

    const term = new Terminal({ cursorBlink: true, fontSize: 14 });
    const fitAddon = new FitAddon();
    term.loadAddon(fitAddon);
    term.open(document.getElementById('terminal'));
    fitAddon.fit();

    const statusEl = document.getElementById('status');
    const sandboxId = '${sandboxId}';
    const sessionId = '${sessionId}';

    let ws = null;
    let reconnectAttempts = 0;
    const maxReconnectAttempts = 10;

    function updateStatus(status) {
      statusEl.textContent = status;
      statusEl.dataset.testid = 'connection-status';
    }

    function connect() {
      updateStatus('connecting');
      const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
      const wsUrl = protocol + '//' + location.host + '/terminal/' + sessionId + '?sandboxId=' + sandboxId;
      
      ws = new WebSocket(wsUrl);
      ws.binaryType = 'arraybuffer';

      ws.onopen = () => {
        term.onData(data => {
          if (ws?.readyState === WebSocket.OPEN) {
            ws.send(new TextEncoder().encode(data));
          }
        });

        term.onResize(({ cols, rows }) => {
          if (ws?.readyState === WebSocket.OPEN) {
            ws.send(JSON.stringify({ type: 'resize', cols, rows }));
          }
        });
      };

      ws.onmessage = (event) => {
        if (event.data instanceof ArrayBuffer) {
          term.write(new Uint8Array(event.data));
        } else if (typeof event.data === 'string') {
          try {
            const msg = JSON.parse(event.data);
            if (msg.type === 'ready') {
              reconnectAttempts = 0;
              updateStatus('connected');
              ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
            } else if (msg.type === 'error') {
              console.error('Server error:', msg.message);
            }
          } catch {}
        }
      };

      ws.onclose = () => {
        updateStatus('disconnected');
        if (reconnectAttempts < maxReconnectAttempts) {
          const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 30000);
          reconnectAttempts++;
          setTimeout(connect, delay);
        }
      };

      ws.onerror = () => {
        console.error('WebSocket error');
      };
    }

    window.addEventListener('resize', () => fitAddon.fit());
    window.terminalConnect = connect;
    window.terminalDisconnect = () => { ws?.close(); ws = null; };
    window.testCloseWs = () => { ws?.close(); };

    connect();
  </script>
</body>
</html>`;
}
