import { ensureEngineAdapter } from './engine-adapter.js';
import {
  normalizeDifficulty,
  normalizeSettings,
  normalizeTeam,
  sanitizeEngineNick
} from './menu-config.js';
import { sha256Blob } from './sha256.js';

const DEFAULT_ENGINE_CANDIDATES = Object.freeze([
  '/engine/openq4.js',
  '/engine/openQ4.js',
  '/engine/quake4.js',
  '/engine/d3wasm.js'
]);
const TEAM_GAME_MODES = new Set(['Team DM', 'CTF', 'Arena CTF']);
const guardedPointerLockTargets = new WeakSet();

function normalizedRetailMap(value) {
  const map = String(value || '').trim().toLowerCase().replace(/^maps\//, '');
  return map && !map.includes('/') ? `mp/${map}` : map;
}

function requestedRetailMap(launch) {
  if (launch?.type === 'multiplayer') return normalizedRetailMap(launch.match?.map || launch.map);
  if (launch?.type === 'replay') {
    return normalizedRetailMap(launch.replay?.metadata?.map || launch.replay?.map || launch.watch?.map);
  }
  return '';
}

export function guardPointerLockPromise(target) {
  if (!target || guardedPointerLockTargets.has(target) || typeof target.requestPointerLock !== 'function') return target;
  const requestPointerLock = target.requestPointerLock;
  Object.defineProperty(target, 'requestPointerLock', {
    configurable: true,
    writable: true,
    value(...args) {
      let result;
      try {
        result = requestPointerLock.apply(this, args);
      } catch (error) {
        result = Promise.reject(error);
      }
      result?.catch?.(() => {});
      return result;
    }
  });
  guardedPointerLockTargets.add(target);
  return target;
}

export class EngineLaunchError extends Error {
  constructor(code, message, details = {}) {
    super(message);
    this.name = 'EngineLaunchError';
    this.code = code;
    this.details = details;
  }
}

export function websocketEndpoint(
  port,
  locationObject = globalThis.location,
  reservationToken = null,
  replayClaimToken = null
) {
  const numericPort = Number(port);
  if (!Number.isInteger(numericPort) || numericPort < 1024 || numericPort > 65535) {
    throw new EngineLaunchError('INVALID_SERVER_PORT', 'Port serwera musi mieścić się w zakresie 1024–65535.');
  }
  const protocol = locationObject.protocol === 'https:' ? 'wss:' : 'ws:';
  const endpoint = new URL(`${protocol}//${locationObject.host}/game/${numericPort}`);
  if (reservationToken !== null && reservationToken !== undefined) {
    const token = String(reservationToken);
    if (!/^[a-f0-9]{64}$/.test(token)) {
      throw new EngineLaunchError('MATCH_RESERVATION_TOKEN_INVALID', 'Token rezerwacji meczu jest nieprawidłowy.');
    }
    endpoint.searchParams.set('reservation', token);
  }
  if (replayClaimToken !== null && replayClaimToken !== undefined) {
    const token = String(replayClaimToken);
    if (!/^[a-f0-9]{64}$/.test(token)) {
      throw new EngineLaunchError('REPLAY_CLAIM_TOKEN_INVALID', 'Token uruchomienia powtórki jest nieprawidłowy.');
    }
    endpoint.searchParams.set('replayClaim', token);
  }
  return endpoint.href;
}

export function launchArguments(launch, retail, runtime = retail) {
  const type = launch?.type || 'singleplayer';
  if (!['singleplayer', 'multiplayer', 'replay'].includes(type)) {
    throw new EngineLaunchError('INVALID_LAUNCH_TYPE', 'Nieznany tryb uruchomienia silnika.');
  }
  const multiplayer = type === 'multiplayer' || type === 'replay';
  const settings = normalizeSettings(launch?.settings);
  const args = [
    '+set', 'fs_basepath', retail.basePath,
    '+set', 'fs_cdpath', runtime.basePath,
    '+set', 'fs_savepath', '/opfs/q4-user',
    '+set', 'fs_caseSensitiveOS', '0',
    '+set', 'r_fullscreen', '0',
    '+set', 'r_swapInterval', '1',
    '+set', 'si_gameType', multiplayer ? 'DM' : 'singleplayer',
    '+set', 'sensitivity', String(settings.sensitivity),
    '+set', 'm_pitch', settings.invertY ? '-0.022' : '0.022',
    '+set', 'g_fov', String(settings.fov),
    '+set', 's_volume', String(settings.masterVolume),
    '+set', 's_musicVolume', String(settings.musicVolume)
  ];
  if (retail?.bundled === true) {
    if (retail.performancePreset !== 'balanced' || retail.maxMachineSpec !== 2) {
      throw new EngineLaunchError('INVALID_RETAIL_BUNDLE_PROFILE', 'Pakiet mapy nie określa bezpiecznego profilu grafiki.');
    }
    const descriptorMap = normalizedRetailMap(retail.map);
    const launchMap = requestedRetailMap(launch);
    if (!descriptorMap || (launchMap && launchMap !== descriptorMap)) {
      throw new EngineLaunchError(
        'RETAIL_BUNDLE_MAP_MISMATCH',
        'Aktywny pakiet danych nie odpowiada wybranej mapie.',
        { expected: launchMap || null, active: descriptorMap || null }
      );
    }
    args.push(
      '+set', 'fs_validateOfficialPaks', '0',
      '+set', 'com_machineSpec', String(retail.maxMachineSpec),
      '+applyPerformancePreset', retail.performancePreset
    );
  }
  if (!multiplayer) {
    args.push(
      '+set', 'com_skipLoadingContinue', '1',
      '+set', 'g_skill', String(normalizeDifficulty(launch?.difficulty))
    );
  }
  if (launch?.nick) args.push('+set', 'ui_name', sanitizeEngineNick(launch.nick));
  if (type === 'multiplayer') {
    const requestedTeam = normalizeTeam(launch?.profile?.team);
    const teamMode = TEAM_GAME_MODES.has(String(launch?.match?.mode ?? launch?.match?.gameType ?? ''));
    const team = teamMode && requestedTeam === 'auto' ? 'Marine' : requestedTeam;
    const model = /^model_player_[a-z0-9_]+$/.test(String(launch?.profile?.model || ''))
      ? String(launch.profile.model)
      : null;
    const modelTeam = normalizeTeam(launch?.profile?.modelTeam);
    if (team !== 'auto') args.push('+set', 'ui_team', team);
    if (model) {
      args.push('+set', 'ui_model', model);
      if (modelTeam === 'Marine') args.push('+set', 'ui_model_marine', model);
      if (modelTeam === 'Strogg') args.push('+set', 'ui_model_strogg', model);
    }
    args.push('+set', 'ui_autoJoin', '1');
  }
  if (multiplayer) {
    args.push('+connect', `127.0.0.1:${launch.port}`);
  }
  return args;
}

function variantManifest(manifest, mode) {
  if (!manifest?.variants) return manifest;
  const key = mode === 'replay' ? 'replay' : mode === 'multiplayer' ? 'multiplayer' : 'singleplayer';
  const selected = manifest.variants[key] || (key === 'replay' ? manifest.variants.multiplayer : null);
  if (!selected || typeof selected !== 'object') {
    throw new EngineLaunchError('ENGINE_VARIANT_MISSING', `Manifest silnika nie zawiera wariantu ${key}.`);
  }
  return {
    ...selected,
    build: selected.build || manifest.build || null,
    schema: selected.schema || manifest.schema || null
  };
}

function normalizeEngineManifest(rawManifest, baseUrl, mode = 'singleplayer') {
  const manifest = variantManifest(rawManifest, mode);
  if (!manifest || typeof manifest.script !== 'string') {
    throw new EngineLaunchError('INVALID_ENGINE_MANIFEST', 'Manifest silnika nie wskazuje pliku startowego.');
  }
  const script = new URL(manifest.script, `${baseUrl}/engine/`);
  const origin = new URL(baseUrl).origin;
  if (script.origin !== origin || !script.pathname.startsWith('/engine/')) {
    throw new EngineLaunchError('INVALID_ENGINE_MANIFEST', 'Plik startowy silnika musi pochodzić z katalogu /engine/.');
  }
  let wasm = null;
  if (typeof manifest.wasm === 'string') {
    const wasmUrl = new URL(manifest.wasm, `${baseUrl}/engine/`);
    if (wasmUrl.origin !== origin || !wasmUrl.pathname.startsWith('/engine/')) {
      throw new EngineLaunchError('INVALID_ENGINE_MANIFEST', 'Moduł WASM musi pochodzić z katalogu /engine/.');
    }
    wasm = `${wasmUrl.pathname}${wasmUrl.search}`;
  }
  let gameLib = null;
  if (manifest.gameLib && typeof manifest.gameLib === 'object') {
    const expectedFilename = mode === 'singleplayer' ? 'game-sp_wasm.wasm' : 'game-mp_wasm.wasm';
    if (manifest.gameLib.filename !== expectedFilename || typeof manifest.gameLib.url !== 'string') {
      throw new EngineLaunchError('INVALID_ENGINE_MANIFEST', `Manifest silnika nie wskazuje właściwego modułu ${expectedFilename}.`);
    }
    const gameLibUrl = new URL(manifest.gameLib.url, `${baseUrl}/engine/`);
    if (gameLibUrl.origin !== origin || gameLibUrl.pathname !== `/engine/${expectedFilename}`) {
      throw new EngineLaunchError('INVALID_ENGINE_MANIFEST', 'GameLib musi pochodzić z katalogu /engine/.');
    }
    const sha256 = String(manifest.gameLib.sha256 || '').toLowerCase();
    if (!/^[a-f0-9]{64}$/.test(sha256)) {
      throw new EngineLaunchError('INVALID_ENGINE_MANIFEST', 'Manifest GameLib nie zawiera prawidłowej sumy SHA-256.');
    }
    gameLib = {
      source: typeof manifest.gameLib.source === 'string' ? manifest.gameLib.source : null,
      url: `${gameLibUrl.pathname}${gameLibUrl.search}`,
      filename: expectedFilename,
      sha256
    };
  } else if (Number(manifest.schema) >= 2) {
    throw new EngineLaunchError('INVALID_ENGINE_MANIFEST', 'Manifest silnika nie zawiera odrębnego modułu GameLib.');
  }
  return {
    script: `${script.pathname}${script.search}`,
    module: Boolean(manifest.module || script.pathname.endsWith('.mjs')),
    factory: typeof manifest.factory === 'string' ? manifest.factory : null,
    wasm,
    gameLib,
    arguments: Array.isArray(manifest.arguments) ? manifest.arguments.map(String) : [],
    build: typeof manifest.build === 'string' ? manifest.build : null,
    readySignal: manifest.readySignal === 'engine' ? 'engine' : 'runtime'
  };
}

function appendHook(existing, hook) {
  const hooks = Array.isArray(existing) ? [...existing] : existing ? [existing] : [];
  hooks.push(hook);
  return hooks;
}

export function engineLogMilestone(value) {
  const line = String(value || '').trim();
  if (!line) return null;
  if (/file system initialized\.?$/i.test(line)) {
    return { stage: 'engine-filesystem', message: 'Dane gry zostały zindeksowane.' };
  }
  if (/renderSystem initialized\.?$/i.test(line)) {
    return { stage: 'engine-renderer', message: 'Przygotowanie renderera WebGL 2…' };
  }
  if (/sound system initialized\.?$/i.test(line)) {
    return { stage: 'engine-audio', message: 'System dźwięku jest gotowy.' };
  }
  if (/renderer startup phase:\s*renderer startup complete/i.test(line)) {
    return { stage: 'engine-renderer', message: 'Renderer WebGL 2 jest gotowy.' };
  }
  const rendererPhase = /renderer startup phase:\s*(.+)$/i.exec(line)?.[1]?.trim();
  if (rendererPhase) {
    const message = {
      R_InitOpenGL: 'Tworzenie kontekstu WebGL 2…',
      R_CheckPortableExtensions: 'Sprawdzanie funkcji WebGL 2…',
      'vertexCache.Init': 'Przygotowanie pamięci geometrii…',
      'tr.SetBackEndRenderer': 'Uruchamianie shaderów…'
    }[rendererPhase] || `Przygotowanie renderera: ${rendererPhase}`;
    return { stage: 'engine-renderer', message };
  }
  if (/game initialized\.?$/i.test(line)) {
    return { stage: 'engine-game', message: 'Moduł zasad gry jest gotowy.' };
  }
  if (/session initialized\.?$/i.test(line)) {
    return { stage: 'engine-session', message: 'Tworzenie sesji gry…' };
  }
  return null;
}

export class EngineLoader {
  constructor({
    fetchImpl = globalThis.fetch?.bind(globalThis),
    documentObject = globalThis.document,
    globalObject = globalThis,
    locationObject = globalThis.location,
    // Cold WASM compilation plus OpenQ4 framework and retail scans can take minutes.
    timeoutMs = 30 * 60_000
  } = {}) {
    this.fetchImpl = fetchImpl;
    this.documentObject = documentObject;
    this.globalObject = globalObject;
    this.locationObject = locationObject;
    this.timeoutMs = timeoutMs;
    this.launching = false;
    this.ready = false;
    this.tainted = false;
  }

  baseUrl() {
    if (this.locationObject?.origin) return this.locationObject.origin;
    return `${this.locationObject?.protocol || 'http:'}//${this.locationObject?.host || 'localhost'}`;
  }

  async discover(mode = 'singleplayer') {
    if (typeof this.fetchImpl !== 'function') {
      throw new EngineLaunchError('ENGINE_FETCH_UNAVAILABLE', 'Przeglądarka nie może sprawdzić artefaktów silnika.');
    }
    try {
      const response = await this.fetchImpl('/engine/engine-manifest.json', { cache: 'no-store' });
      if (response.ok) {
        let payload;
        try {
          payload = await response.json();
        } catch (error) {
          throw new EngineLaunchError('INVALID_ENGINE_MANIFEST', 'Manifest silnika nie jest prawidłowym JSON-em.', { cause: error.message });
        }
        return normalizeEngineManifest(payload, this.baseUrl(), mode);
      }
    } catch (error) {
      if (error instanceof EngineLaunchError) throw error;
    }

    for (const script of DEFAULT_ENGINE_CANDIDATES) {
      try {
        const response = await this.fetchImpl(script, { method: 'HEAD', cache: 'no-store' });
        if (response.ok) return normalizeEngineManifest({ script }, this.baseUrl());
      } catch (_error) {
        // Try the next conventional Emscripten output name.
      }
    }
    throw new EngineLaunchError(
      'ENGINE_MISSING',
      'Silnik OpenQ4 WebAssembly nie jest jeszcze zbudowany na tym serwerze.',
      { candidates: DEFAULT_ENGINE_CANDIDATES }
    );
  }

  locateFile(path, manifest) {
    if (path.endsWith('.wasm') && manifest.wasm) return new URL(manifest.wasm, `${this.baseUrl()}/engine/`).href;
    return new URL(path, new URL(manifest.script, `${this.baseUrl()}/engine/`)).href;
  }

  async loadArtifact(manifest, moduleConfig) {
    if (manifest.module) {
      const moduleUrl = new URL(manifest.script, this.baseUrl()).href;
      const imported = await import(moduleUrl);
      const factory = manifest.factory ? imported[manifest.factory] : imported.default;
      if (typeof factory === 'function') return factory(moduleConfig);
      return imported;
    }

    if (!this.documentObject?.createElement) {
      throw new EngineLaunchError('DOCUMENT_UNAVAILABLE', 'Nie można załadować skryptu silnika poza przeglądarką.');
    }
    await new Promise((resolve, reject) => {
      const script = this.documentObject.createElement('script');
      script.src = manifest.script;
      script.async = true;
      script.addEventListener('load', resolve, { once: true });
      script.addEventListener('error', () => reject(new EngineLaunchError(
        'ENGINE_SCRIPT_FAILED',
        `Nie udało się załadować ${manifest.script}.`
      )), { once: true });
      this.documentObject.head.append(script);
    });
    return this.globalObject.Module;
  }

  async loadGameLib(gameLib, { onProgress = () => {} } = {}) {
    if (!gameLib) return null;
    let response;
    try {
      // GameLib URLs are content-hashed and still verified below, so retaining the
      // HTTP cache avoids another 19 MiB transfer on every engine restart.
      response = await this.fetchImpl(gameLib.url, { cache: 'force-cache' });
    } catch (error) {
      throw new EngineLaunchError('ENGINE_GAMELIB_FAILED', `Nie udało się pobrać ${gameLib.filename}.`, { cause: error?.message || String(error) });
    }
    if (!response?.ok) {
      throw new EngineLaunchError('ENGINE_GAMELIB_FAILED', `Nie udało się pobrać ${gameLib.filename} (HTTP ${response?.status || 0}).`);
    }
    const declaredBytes = Number(response.headers?.get?.('content-length'));
    const totalBytes = Number.isFinite(declaredBytes) && declaredBytes > 0 ? declaredBytes : 0;
    const reader = response.body?.getReader?.();
    let completedBytes = 0;
    let blob;
    onProgress({ state: 'download', file: gameLib.filename, completedBytes, totalBytes });
    if (reader) {
      const chunks = [];
      try {
        while (true) {
          const { done, value } = await reader.read();
          if (done) break;
          const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
          chunks.push(bytes);
          completedBytes += bytes.byteLength;
          onProgress({ state: 'download', file: gameLib.filename, completedBytes, totalBytes });
        }
      } catch (error) {
        await reader.cancel(error).catch(() => {});
        throw error;
      }
      blob = new Blob(chunks);
    } else {
      blob = await response.blob();
      completedBytes = blob.size;
      onProgress({ state: 'download', file: gameLib.filename, completedBytes, totalBytes: totalBytes || blob.size });
    }
    onProgress({
      state: 'verify',
      file: gameLib.filename,
      completedBytes,
      totalBytes: totalBytes || blob.size,
      indeterminate: true
    });
    const actual = await sha256Blob(blob);
    if (actual !== gameLib.sha256) {
      throw new EngineLaunchError('ENGINE_GAMELIB_INTEGRITY', `Suma SHA-256 ${gameLib.filename} jest nieprawidłowa.`, {
        expected: gameLib.sha256,
        actual
      });
    }
    onProgress({ state: 'ready', file: gameLib.filename, completedBytes, totalBytes: totalBytes || blob.size });
    return { ...gameLib, bytes: new Uint8Array(await blob.arrayBuffer()) };
  }

  async launch({ canvas, launch, importer, onStatus = () => {}, onReady = () => {}, onError = () => {} }) {
    if (this.launching || this.ready) {
      throw new EngineLaunchError('ENGINE_ALREADY_STARTED', 'Silnik został już uruchomiony w tej karcie.');
    }
    this.launching = true;
    guardPointerLockPromise(canvas);
    ensureEngineAdapter(this.globalObject);

    try {
      onStatus({ stage: 'retail', message: 'Sprawdzanie lokalnych danych gry…' });
      const retail = await importer?.launchDescriptor?.();
      if (!retail) {
        throw new EngineLaunchError('RETAIL_MISSING', 'Najpierw zaimportuj własne pliki PK4 z katalogu q4base.');
      }

      onStatus({ stage: 'discover', message: 'Sprawdzanie kompilacji OpenQ4…' });
      const manifest = await this.discover(launch?.type);
      onStatus({ stage: 'runtime', message: 'Sprawdzanie danych runtime OpenQ4…' });
      const runtimeAssetsPromise = typeof importer?.ensureRuntimeAssets === 'function'
        ? importer.ensureRuntimeAssets({
          onProgress: ({ state, completedBytes, totalBytes, file }) => onStatus({
            stage: 'runtime',
            state,
            message: state === 'verify-runtime'
              ? `Weryfikacja ${file}…`
              : file ? `Pobieranie ${file}…` : 'Sprawdzanie danych runtime OpenQ4…',
            completedBytes,
            totalBytes,
            indeterminate: state === 'verify-runtime'
          })
        })
        : { basePath: retail.basePath, build: manifest.build || 'embedded' };
      // Keep independently measured phases sequential so one progress bar never
      // jumps backwards between unrelated byte totals.
      const runtimeAssets = await runtimeAssetsPromise;
      if (manifest.gameLib) onStatus({ stage: 'gamelib', message: `Sprawdzanie ${manifest.gameLib.filename}…` });
      const gameLib = await this.loadGameLib(manifest.gameLib, {
        onProgress: (progress) => onStatus({
          stage: 'gamelib',
          message: progress.state === 'verify'
            ? `Weryfikacja ${progress.file}…`
            : progress.state === 'ready'
              ? `${progress.file} jest gotowy.`
              : `Pobieranie ${progress.file}…`,
          ...progress
        })
      });
      const network = launch?.type === 'multiplayer' || launch?.type === 'replay'
        ? {
            port: Number(launch.port),
            websocketUrl: websocketEndpoint(
              launch.port,
              this.locationObject,
              launch.reservationToken,
              launch.replayClaimToken
            ),
            datagramFraming: 'one-binary-frame-per-udp-datagram',
            spectator: launch.type === 'replay'
          }
        : null;

      let settled = false;
      let timeout;
      let resolveRuntime;
      let rejectRuntime;
      const runtime = new Promise((resolve, reject) => {
        resolveRuntime = resolve;
        rejectRuntime = reject;
      });

      const fail = (error) => {
        if (settled) return;
        settled = true;
        clearTimeout(timeout);
        const normalized = error instanceof Error ? error : new EngineLaunchError('ENGINE_ABORTED', String(error));
        rejectRuntime(normalized);
      };

      const markReady = async (moduleInstance = this.globalObject.Module) => {
        if (settled) return;
        try {
          const adapter = this.globalObject.Q4EngineAdapter;
          if (typeof adapter?.runtimeReady === 'function') {
            await adapter.runtimeReady({ module: moduleInstance, launch, retail, runtime: runtimeAssets, network });
          }
          if (network && typeof adapter?.connect === 'function') {
            await adapter.connect(network);
          }
          if (launch?.type === 'singleplayer' && typeof moduleInstance?.ccall === 'function' &&
              typeof adapter?.execute === 'function') {
            adapter.execute('map game/airdefense1');
          }
          onStatus({
            stage: 'ready',
            message: 'OpenQ4 jest gotowy.',
            completedUnits: 1,
            totalUnits: 1
          });
          settled = true;
          clearTimeout(timeout);
          this.ready = true;
          resolveRuntime({ module: moduleInstance, manifest, retail, runtime: runtimeAssets, network });
        } catch (error) {
          fail(error);
        }
      };

      const existingModule = this.globalObject.Module || {};
      const globalObject = this.globalObject;
      const existingInitialized = existingModule.onRuntimeInitialized;
      const existingAbort = existingModule.onAbort;
      const existingDependencyMonitor = existingModule.monitorRunDependencies;
      const output = [];
      let maximumRunDependencies = 0;
      const moduleConfig = {
        ...existingModule,
        canvas,
        arguments: [...manifest.arguments, ...launchArguments(launch, retail, runtimeAssets)],
        q4WebSocketUrl: network?.websocketUrl || null,
        locateFile: (path) => this.locateFile(path, manifest),
        print: (...parts) => {
          const line = parts.join(' ');
          output.push(line);
          if (output.length > 1000) output.shift();
          existingModule.print?.(...parts);
          const milestone = engineLogMilestone(line);
          if (milestone) onStatus(milestone);
        },
        printErr: (...parts) => {
          const line = parts.join(' ');
          output.push(line);
          if (output.length > 1000) output.shift();
          existingModule.printErr?.(...parts);
          const milestone = engineLogMilestone(line);
          if (milestone) onStatus(milestone);
        },
        monitorRunDependencies: (remaining) => {
          existingDependencyMonitor?.call(existingModule, remaining);
          const pending = Math.max(0, Number(remaining) || 0);
          maximumRunDependencies = Math.max(maximumRunDependencies, pending);
          onStatus({
            stage: 'dependencies',
            message: pending
              ? 'Ładowanie modułów WebAssembly…'
              : 'Moduły WebAssembly są gotowe.',
            completedUnits: Math.max(0, maximumRunDependencies - pending),
            totalUnits: maximumRunDependencies
          });
        },
        setStatus: (message) => {
          existingModule.setStatus?.(message);
          if (message) onStatus({ stage: 'engine', message: String(message) });
        },
        onAbort: (reason) => {
          existingAbort?.(reason);
          fail(new EngineLaunchError('ENGINE_ABORTED', `Silnik przerwał start: ${reason || 'brak szczegółów'}`, { output }));
        },
        onRuntimeInitialized: async function onRuntimeInitialized() {
          await existingInitialized?.call(this);
          if (manifest.readySignal === 'engine') {
            onStatus({ stage: 'engine', message: 'Inicjalizacja frameworka OpenQ4…' });
          } else {
            await markReady(this);
          }
        }
      };

      if (gameLib) {
        moduleConfig.preRun = appendHook(existingModule.preRun, function stageGameLib() {
          if (typeof moduleConfig.FS_createPath !== 'function' || typeof moduleConfig.FS_createDataFile !== 'function') {
            throw new EngineLaunchError('ENGINE_GAMELIB_STAGE_FAILED', 'Runtime WasmFS nie może przygotować modułu GameLib.');
          }
          moduleConfig.FS_createPath('/', 'baseoq4', true, true);
          moduleConfig.FS_createDataFile('/baseoq4', gameLib.filename, gameLib.bytes, true, false, true);
        });
      }

      moduleConfig.preRun = appendHook(moduleConfig.preRun, function mountRetailData() {
        const adapter = globalObject.Q4EngineAdapter;
        if (typeof adapter?.mountRetail !== 'function') return;
        const dependency = 'q4-opfs-retail';
        const addDependency = moduleConfig.addRunDependency || globalObject.addRunDependency;
        const removeDependency = moduleConfig.removeRunDependency || globalObject.removeRunDependency;
        addDependency?.(dependency);
        Promise.resolve(adapter.mountRetail({
          module: moduleConfig,
          FS: moduleConfig.FS || globalObject.FS,
          mountPoint: '/q4base',
          retail
        })).then(() => {
          removeDependency?.(dependency);
        }).catch((error) => {
          removeDependency?.(dependency);
          fail(new EngineLaunchError('RETAIL_MOUNT_FAILED', `Nie udało się zamontować danych gry: ${error.message || error}`));
        });
      });

      this.globalObject.Q4Shell = Object.freeze({
        version: 1,
        launch: Object.freeze({ ...launch }),
        retail,
        runtime: runtimeAssets,
        network,
        logs: () => [...output],
        status: (message) => onStatus({ stage: 'adapter', message: String(message) }),
        markReady,
        fail
      });
      this.globalObject.Module = moduleConfig;

      timeout = setTimeout(() => fail(new EngineLaunchError(
        'ENGINE_TIMEOUT',
        'Silnik nie zgłosił gotowości w wyznaczonym czasie.',
        { timeoutMs: this.timeoutMs, output }
      )), this.timeoutMs);

      onStatus({ stage: 'load', message: `Ładowanie ${manifest.script}…` });
      // An imported Emscripten factory cannot be cancelled reliably. Any failure
      // after this point requires a fresh document before another launch.
      this.tainted = true;
      const artifact = this.loadArtifact(manifest, moduleConfig);
      artifact.catch(fail);
      const [loadedModule, result] = await Promise.all([artifact, runtime]);
      if (loadedModule && loadedModule !== moduleConfig) this.globalObject.Module = loadedModule;
      this.launching = false;
      onReady(result);
      return result;
    } catch (error) {
      this.launching = false;
      const normalized = error instanceof EngineLaunchError
        ? error
        : new EngineLaunchError('ENGINE_START_FAILED', error?.message || String(error), { cause: error });
      onError(normalized);
      throw normalized;
    }
  }
}
