/**
 * `realm-factory.ts` — selects the right realm impl per
 * `(kind, runtime)`:
 *
 *   - `kind:'js'` + standalone → `DedicatedWorker` over
 *     `js-realm-worker.ts` (full eval permissions, no CSP).
 *   - `kind:'js'` + extension → per-task sandbox iframe via
 *     `createIframeRealm` (offscreen CSP blocks AsyncFunction in
 *     workers).
 *   - `kind:'py'` + both → `DedicatedWorker` over
 *     `py-realm-worker.ts` (Pyodide is WASM, only needs
 *     `wasm-unsafe-eval` which both modes grant).
 *
 * The factory shape is `(kind, ctx) => Promise<Realm>`. Callers
 * thread it into `runInRealm` so tests can substitute mocks.
 */

import { createIframeRealm } from './realm-iframe.js';
import { createInProcessJsRealmFactory, createInProcessPyRealmFactory } from './realm-inprocess.js';
import {
  isExtensionRuntime,
  isNodeRuntime,
  resolveNodePackageBaseUrl,
} from '../../shell/supplemental-commands/shared.js';
import { PYODIDE_CDN } from './py-realm-shared.js';
import type { Realm, RealmFactory } from './realm-runner.js';
import type { RealmKind } from './realm-types.js';
import type { RealmPortLike } from './realm-rpc.js';

/**
 * Production realm factory. Inspects runtime + `kind` and returns
 * the matching impl. Pure dispatcher — testable bits live in the
 * impl files (`createIframeRealm`, the worker entries).
 *
 * Fallback chain when the preferred impl isn't available:
 *   - kind:'js' standalone → DedicatedWorker → in-process JS
 *   - kind:'js' extension → sandbox iframe → in-process JS
 *   - kind:'py' both → DedicatedWorker → in-process Pyodide
 *
 * In-process is the vitest/headless-node path. SIGKILL becomes
 * cooperative (no `worker.terminate()` to invoke), but the real
 * floats always have Worker / DOM available so production keeps
 * the hard-kill guarantee.
 */
const inProcessJs = createInProcessJsRealmFactory();
const inProcessPy = createInProcessPyRealmFactory();

export function createDefaultRealmFactory(): RealmFactory {
  return async ({ kind, ctx }) => {
    if (kind === 'py') {
      if (typeof Worker !== 'undefined') return createPyWorkerRealm();
      return inProcessPy({ kind, ctx });
    }
    // kind === 'js'
    if (isExtensionRuntime() && typeof document !== 'undefined') {
      return createIframeRealm(kind, ctx);
    }
    if (typeof Worker !== 'undefined') return createJsWorkerRealm();
    return inProcessJs({ kind, ctx });
  };
}

// ---------------------------------------------------------------------------
// Worker impls (standalone JS, both-mode Python)
// ---------------------------------------------------------------------------

function createJsWorkerRealm(): Realm {
  if (typeof Worker === 'undefined') {
    throw new Error('realm-factory: Worker is not available in this runtime');
  }
  const worker = new Worker(new URL('./js-realm-worker.ts', import.meta.url), { type: 'module' });
  return wrapWorker(worker);
}

function createPyWorkerRealm(): Realm {
  if (typeof Worker === 'undefined') {
    throw new Error('realm-factory: Worker is not available in this runtime');
  }
  const worker = new Worker(new URL('./py-realm-worker.ts', import.meta.url), { type: 'module' });
  // The Python worker reads `pyodideIndexURL` from the init
  // message; the kernel side picks the right URL based on runtime
  // (extension → bundled, node → node_modules, browser → CDN).
  return wrapWorker(worker);
}

function wrapWorker(worker: Worker): Realm {
  const port: RealmPortLike = {
    postMessage: (msg, transfer) =>
      transfer ? worker.postMessage(msg, transfer) : worker.postMessage(msg),
    addEventListener: (type, handler) => worker.addEventListener(type, handler),
    removeEventListener: (type, handler) => worker.removeEventListener(type, handler),
  };
  let terminated = false;
  return {
    controlPort: port,
    addEventListener: (type, handler, options) => worker.addEventListener(type, handler, options),
    removeEventListener: (type, handler) => worker.removeEventListener(type, handler),
    terminate(): void {
      if (terminated) return;
      terminated = true;
      try {
        worker.terminate();
      } catch {
        /* idempotent */
      }
    },
  };
}

// ---------------------------------------------------------------------------
// Pyodide URL resolution
// ---------------------------------------------------------------------------

/**
 * Pick the Pyodide indexURL for the current runtime. Used by
 * `python-command` to populate `RealmInitMsg.pyodideIndexURL` so
 * the worker side stays runtime-agnostic.
 *
 * Runtime detection MUST go extension → node → browser, in that
 * order. The historical `typeof window === 'undefined'` shortcut
 * misidentifies DedicatedWorkers (no `window`, but still a browser
 * context) as Node and steers them at the local `node_modules`
 * tree, which the Vite dev server returns the SPA fallback for —
 * the worker then tries to load `<!DOCTYPE …>` as a WASM module.
 */
export function resolvePyodideIndexURL(): string {
  if (isExtensionRuntime()) {
    const c = (globalThis as { chrome?: { runtime?: { getURL?: (path: string) => string } } })
      .chrome;
    if (c?.runtime?.getURL) return c.runtime.getURL('pyodide/');
  }
  if (isNodeRuntime()) {
    return decodeURIComponent(
      resolveNodePackageBaseUrl('pyodide/pyodide.mjs', '../../../../../node_modules/pyodide/')
        .pathname
    );
  }
  return PYODIDE_CDN;
}

export type { RealmFactory, Realm, RealmKind };
