import {
  buildLatexCompileResult,
  planBibliographyPasses,
  type CompileArtifact,
  type LatexCompileFile,
  type LatexCompileProgress,
  type LatexCompileRequest,
  type LatexCompileResult,
  type LatexCompiler,
  type LatexCompilerCapabilities,
  type LatexCompilerRuntimeDiagnostics,
} from '@learnitive/latex-compiler';
import type { LearnitiveCompiler } from '@learnitive/project-storage';
import { abortable } from './abortable';
import { loadCompilerDeploymentStatus } from './deployment-status';
import { ensureTexliveServiceWorker } from './texlive-service-worker';
import {
  persistRemoteCache,
  restoreRemoteCache,
  type TexliveRemoteFile,
} from './busytex-remote-cache';

const DEFAULT_MODULE_URL = '/compiler/busytex/index.js';
const DEFAULT_BASE_PATH = '/compiler/busytex';
const DEFAULT_TEXLIVE_ENDPOINT = 'https://texlive2026.texlyre.org/';
const LEARNITIVE_BUSYTEXT_ADAPTER_RELEASE = 'buildfix25';
let busyTexAdapterReleaseLogged = false;

type BusyResult = {
  success: boolean;
  exitCode: number;
  pdf?: Uint8Array;
  log: string;
  synctex?: Uint8Array;
};

type BusyProjectFile = {
  path: string;
  content: Uint8Array | string;
};


type BusyDriver =
  | 'pdftex_bibtex8'
  | 'xetex_bibtex8_dvipdfmx'
  | 'luahbtex_bibtex8';

type BusyRunner = {
  initialize(useWorker?: boolean): Promise<void>;
  isInitialized(): boolean;
  compile(
    files: BusyProjectFile[],
    mainTexPath: string,
    bibtex?: boolean | null,
    biber?: boolean | null,
    makeindex?: boolean | null,
    rerun?: boolean | null,
    verbose?: 'silent' | 'info' | 'debug',
    driver?: BusyDriver,
    dataPackagesJs?: string[] | null,
    remoteEndpoint?: string,
    shellEscape?: boolean,
    shellHandlerScripts?: string[],
  ): Promise<BusyResult>;
  readProjectFiles?(path?: string): Promise<BusyProjectFile[]>;
  writeTexliveRemoteFiles?(files: TexliveRemoteFile[]): Promise<void>;
  writeTexliveRemoteMisses?(misses: string[]): Promise<void>;
  terminate(): void;
};

type BusyModule = {
  BusyTexRunner: new (options: {
    busytexBasePath: string;
    verbose: boolean;
    engineMode: 'combined';
    preloadDataPackages: string[];
    catalogDataPackages: string[];
    onDownloadProgress?: (progress: { percent?: number }) => void;
  }) => BusyRunner;
};

function bibliographyPlanFor(request: LatexCompileRequest) {
  return request.auxiliaryPasses === 'none'
    ? { bibtex: false, biberRequested: false, makeindex: false, rerun: false }
    : planBibliographyPasses(request.files);
}

const BUSYTEX_DRIVER_BY_COMPILER: Record<LearnitiveCompiler, BusyDriver> = {
  pdflatex: 'pdftex_bibtex8',
  xelatex: 'xetex_bibtex8_dvipdfmx',
  lualatex: 'luahbtex_bibtex8',
};

type BusyTexRuntimeAbiMode =
  | 'native-v14'
  | 'legacy-worker-shim'
  | 'legacy-worker-bridge';

type BusyTexRuntimeAbi = {
  mode: BusyTexRuntimeAbiMode;
  supportsBiber: boolean;
  parameters: readonly string[];
};

const BUSYTEX_LEGACY_SHIM_MARKER = 'LEARNITIVE_BUSYTEXT_LEGACY_NO_BIBER_SHIM_V1';
const BUSYTEX_LEGACY_PIPELINE_SEQUENCE = [
  'bibtex',
  'makeindex',
  'rerun',
  'verbose',
  'driver',
] as const;

async function importBusyModule(url: string): Promise<BusyModule> {
  // Vite must leave this deployment asset as a runtime URL; the installer can
  // refresh compiler assets independently of the main application bundle.
  try {
    return (await import(/* @vite-ignore */ url)) as BusyModule;
  } catch (error) {
    throw new Error('Failed to load BusyTeX runtime module', { cause: error });
  }
}

const BUSYTEX_TEXT_PROJECT_FILE = /\.(?:tex|ltx|sty|cls|bib|bst|cfg|def|clo|fd|bbx|cbx|lbx|ist)$/iu;
const busyTexSourceDecoder = new TextDecoder('utf-8');

function cloneFiles(files: readonly LatexCompileFile[]): BusyProjectFile[] {
  return files.map((file) => ({
    path: file.path,
    content: BUSYTEX_TEXT_PROJECT_FILE.test(file.path)
      ? busyTexSourceDecoder.decode(file.content)
      : file.content.slice(),
  }));
}

const TEXLIVE_REMOTE_RETRYABLE_EXTENSION =
  /\.(?:sty|cls|clo|cfg|def|fd|bbx|cbx|lbx|bst|enc|map|tfm|vf|pfb|otf|ttf|lua|tex)$/iu;

function missingTexliveDependency(
  log: string,
  projectFileNames: ReadonlySet<string>,
): string | undefined {
  const candidates = [
    ...log.matchAll(/File [`']([^`'\r\n]+)[`'] not found/giu),
    ...log.matchAll(/I can't find file [`']([^`'\r\n]+)[`']/giu),
  ];
  for (const match of candidates) {
    const requested = match[1]?.trim();
    if (!requested || !TEXLIVE_REMOTE_RETRYABLE_EXTENSION.test(requested)) continue;
    const normalized = requested.replace(/\\/gu, '/');
    const base = normalized.slice(normalized.lastIndexOf('/') + 1);
    if (projectFileNames.has(base)) continue;
    return requested;
  }
  return undefined;
}

function toBytes(content: Uint8Array | string): Uint8Array {
  return typeof content === 'string' ? new TextEncoder().encode(content) : content;
}


const GENERATED_TEXT_EXTENSIONS = new Set(['aux', 'bbl', 'blg', 'toc', 'out', 'json', 'jsonl']);

type GeneratedArtifactCollection = {
  artifacts: CompileArtifact[];
  readback: NonNullable<LatexCompilerRuntimeDiagnostics['artifactReadback']>;
};

function normalizeBusyProjectPath(path: string, requestedPath?: string): string {
  const normalized = path.replace(/\\/gu, '/').replace(/^\/+/, '');
  const requested = requestedPath?.replace(/\\/gu, '/').replace(/^\/+|\/+$/gu, '');
  if (!requested || !normalized || normalized === requested || normalized.startsWith(`${requested}/`)) {
    return normalized;
  }
  // Some BusyTeX builds return paths relative to the requested directory while
  // others return project-root-relative paths. Only prefix the dedicated Live
  // PDF probe directory, whose fallback read is intentionally directory-scoped.
  return requested === '__learnitive_live__' ? `${requested}/${normalized}` : normalized;
}

async function collectGeneratedArtifacts(
  runner: BusyRunner,
  sourceFiles: readonly LatexCompileFile[],
): Promise<GeneratedArtifactCollection> {
  const attempts: Array<{ path: string; ok: boolean; fileCount: number; error?: string }> = [];
  if (!runner.readProjectFiles) {
    return { artifacts: [], readback: { attempts, mergedFileCount: 0 } };
  }

  const merged = new Map<string, BusyProjectFile>();
  const read = async (requestedPath?: string) => {
    const displayPath = requestedPath ?? '<default>';
    try {
      const files = requestedPath === undefined
        ? await runner.readProjectFiles?.()
        : await runner.readProjectFiles?.(requestedPath);
      const safeFiles = files ?? [];
      attempts.push({ path: displayPath, ok: true, fileCount: safeFiles.length });
      for (const file of safeFiles) {
        const path = normalizeBusyProjectPath(file.path, requestedPath);
        if (!path) continue;
        // Prefer the first successful representation of a path. The default
        // and root reads are generally the least ambiguous; the directory read
        // exists only as a compatibility fallback for Live PDF artifacts.
        if (!merged.has(path)) merged.set(path, { ...file, path });
      }
    } catch (error) {
      attempts.push({
        path: displayPath,
        ok: false,
        fileCount: 0,
        error: errorChain(error),
      });
    }
  };

  await read();
  await read('/');
  const hasLiveRecord = [...merged.keys()].some(
    (path) => path === 'layout.jsonl' || path === '__learnitive_live__/layout.jsonl',
  );
  if (!hasLiveRecord) await read('__learnitive_live__');

  const sourcePaths = new Set(sourceFiles.map((file) => file.path.replace(/^\/+/, '')));
  const decoder = new TextDecoder();
  const artifacts: CompileArtifact[] = [];
  for (const file of merged.values()) {
    const path = file.path.replace(/^\/+/, '');
    if (!path || path.startsWith('tmp/texlive_remote/') || sourcePaths.has(path)) continue;
    const extension = path.includes('.') ? path.slice(path.lastIndexOf('.') + 1).toLowerCase() : '';
    if (!GENERATED_TEXT_EXTENSIONS.has(extension)) continue;
    const data = toBytes(file.content).slice();
    const structuredText = extension === 'json' || extension === 'jsonl';
    artifacts.push({
      path,
      kind: structuredText
        ? 'other'
        : (extension as 'aux' | 'bbl' | 'blg' | 'toc' | 'out'),
      mimeType: structuredText
        ? extension === 'json'
          ? 'application/json'
          : 'application/x-ndjson'
        : 'text/plain;charset=utf-8',
      size: data.byteLength,
      generated: true,
      text: true,
      data,
      textContent: decoder.decode(data),
    });
  }
  return {
    artifacts,
    readback: { attempts, mergedFileCount: merged.size },
  };
}

function errorChain(error: unknown): string {
  const messages: string[] = [];
  let current: unknown = error;
  while (current instanceof Error && messages.length < 5) {
    if (current.message && !messages.includes(current.message)) {
      messages.push(current.message);
    }
    current = current.cause;
  }
  if (messages.length > 0) return messages.join(' → ');
  return String(error);
}

function versionedRuntimeUrl(url: string, fingerprint?: string | null): string {
  if (!fingerprint) return url;
  const separator = url.includes('?') ? '&' : '?';
  return `${url}${separator}learnitive_asset=${encodeURIComponent(fingerprint.slice(0, 16))}`;
}

async function probeAsset(url: string, signal?: AbortSignal): Promise<void> {
  const response = await fetch(url, {
    method: 'HEAD',
    cache: 'no-store',
    signal,
    headers: { accept: '*/*' },
  });
  if (!response.ok) {
    throw new Error(`BusyTeX asset ${url} returned HTTP ${response.status}`);
  }
}

async function assertBusyTexDeploymentAssets(
  status: LatexCompilerCapabilities,
  compiler: LearnitiveCompiler,
  signal?: AbortSignal,
): Promise<void> {
  const assets = status.runtimeAssets?.busytex;
  if (!assets) {
    throw new Error(
      'BusyTeX deployment metadata is missing from /compiler/status.json',
    );
  }
  const required = [
    ...assets.requiredFiles,
    ...(assets.engineRequiredFiles[compiler] ?? []),
  ];
  const unique = [...new Set(required)];
  const failures: string[] = [];
  await Promise.all(
    unique.map(async (url) => {
      try {
        await probeAsset(url, signal);
      } catch (error) {
        failures.push(errorChain(error));
      }
    }),
  );
  signal?.throwIfAborted();
  if (failures.length > 0) {
    throw new Error(
      `BusyTeX deployed asset preflight failed for ${compiler}: ${failures.join('; ')}`,
    );
  }
}

const BUSYTEX_EXPECTED_PIPELINE_SEQUENCE = [
  'bibtex',
  'biber',
  'makeindex',
  'rerun',
  'verbose',
  'driver',
] as const;

function busyTexCompileParameterNames(source: string): string[] {
  const match = /async\s+compile\s*\(([\s\S]*?)\)\s*\{/u.exec(source);
  const parameterList = match?.[1];
  if (parameterList === undefined) return [];
  return parameterList
    .split(',')
    .map((entry) => entry.split('=')[0]?.trim())
    .filter((entry): entry is string => Boolean(entry));
}

function hasOrderedBusyTexSequence(
  parameters: readonly string[],
  expected: readonly string[],
): boolean {
  let cursor = 0;
  for (const parameter of parameters) {
    if (parameter === expected[cursor]) cursor += 1;
    if (cursor === expected.length) return true;
  }
  return false;
}

const BUSYTEX_WORKER_BRIDGE_URL = '/compiler/learnitive-busytex-worker-bridge.js';

function busyTexWorkerBridgeUrl(
  basePath: string,
  runtimeAbi: BusyTexRuntimeAbi,
  engine: LearnitiveCompiler,
): string {
  const url = new URL(BUSYTEX_WORKER_BRIDGE_URL, globalThis.location.href);
  url.searchParams.set('base', basePath);
  url.searchParams.set('abi', runtimeAbi.mode);
  url.searchParams.set('engine', engine);
  url.searchParams.set('learnitive_bridge', 'buildfix25');
  return url.href;
}

function busyTexWorkerRequest(value: string | URL): boolean {
  const href = String(value);
  // texlyre-busytex releases have used both a direct busytex_worker.js URL and
  // an inline Blob-backed worker. initializeBusyTexWithWorkerBridge installs
  // this Worker override only for the duration of BusyTeX initialization, so
  // a blob: worker created in this narrow window is the BusyTeX worker and
  // must be redirected to Learnitive's same-origin compatibility bridge too.
  return (
    /(?:^|\/)busytex_worker\.js(?:[?#].*)?$/u.test(href) ||
    /^blob:/iu.test(href)
  );
}

async function initializeBusyTexWithWorkerBridge(
  runner: BusyRunner,
  basePath: string,
  runtimeAbi: BusyTexRuntimeAbi,
  engine: LearnitiveCompiler,
  signal?: AbortSignal,
): Promise<void> {
  if (typeof Worker !== 'function') {
    throw new Error('BusyTeX Full / SyncTeX mode requires Web Worker support.');
  }
  const nativeWorker = Worker;
  const bridgeUrl = busyTexWorkerBridgeUrl(basePath, runtimeAbi, engine);
  const WorkerBridge = function (
    scriptURL: string | URL,
    options?: WorkerOptions,
  ): Worker {
    return new nativeWorker(
      busyTexWorkerRequest(scriptURL) ? bridgeUrl : scriptURL,
      options,
    );
  } as unknown as typeof Worker;

  if (!Reflect.set(globalThis, 'Worker', WorkerBridge)) {
    throw new Error('BusyTeX could not install its isolated worker compatibility bridge.');
  }

  try {
    await abortable(runner.initialize(true), signal, () => runner.terminate());
  } finally {
    Reflect.set(globalThis, 'Worker', nativeWorker);
  }
}

async function inspectBusyTexRuntimeAbi(
  basePath: string,
  signal?: AbortSignal,
): Promise<BusyTexRuntimeAbi> {
  const response = await fetch(`${basePath}/busytex_pipeline.js`, {
    cache: 'no-store',
    signal,
    headers: { accept: 'text/javascript,*/*;q=0.1' },
  });
  if (!response.ok) {
    throw new Error(
      `BusyTeX runtime ABI preflight could not read busytex_pipeline.js (HTTP ${response.status})`,
    );
  }
  const source = await response.text();
  const parameters = busyTexCompileParameterNames(source);
  const nativeV14 = hasOrderedBusyTexSequence(
    parameters,
    BUSYTEX_EXPECTED_PIPELINE_SEQUENCE,
  );
  const legacyNoBiber =
    !nativeV14 &&
    hasOrderedBusyTexSequence(parameters, BUSYTEX_LEGACY_PIPELINE_SEQUENCE);
  if (!nativeV14 && !legacyNoBiber) {
    throw new Error(
      'BusyTeX runtime ABI mismatch: the deployed pipeline has an unsupported compile signature. ' +
        `Expected either bibtex, biber, makeindex, rerun, verbose, driver or the supported legacy bibtex, makeindex, rerun, verbose, driver ABI; detected [${parameters.join(', ')}].`,
    );
  }
  const legacyShim = source.includes(BUSYTEX_LEGACY_SHIM_MARKER);
  return {
    mode: legacyNoBiber
      ? 'legacy-worker-bridge'
      : legacyShim
        ? 'legacy-worker-shim'
        : 'native-v14',
    supportsBiber: nativeV14 && !legacyShim,
    parameters,
  };
}

export class BusyTexBrowserCompiler implements LatexCompiler {
  readonly backend = 'busytex' as const;

  private runner: BusyRunner | undefined;
  private loading: Promise<void> | undefined;
  private progressListener:
    | ((progress: LatexCompileProgress) => void)
    | undefined;
  private packageTelemetry:
    | { startedAt?: number; lastAt?: number; events: number }
    | undefined;
  private readonly restoredRemoteNames = new Set<string>();
  private initializationMode: 'worker' | undefined;
  private initializedWithRecommended = false;
  private deploymentStatus: LatexCompilerCapabilities | undefined;
  private runtimeAbi: BusyTexRuntimeAbi | undefined;
  private runtime: LatexCompilerRuntimeDiagnostics = {
    status: 'idle',
    backend: 'busytex',
    serviceWorker: 'unknown',
    packageResolutionChain: [],
  };

  constructor(
    private readonly options: {
      moduleUrl?: string;
      basePath?: string;
      texliveEndpoint?: string;
    } = {},
  ) {}

  supports(_compiler: LearnitiveCompiler): boolean {
    return true;
  }

  getRuntimeDiagnostics(): LatexCompilerRuntimeDiagnostics {
    return {
      ...this.runtime,
      packageResolutionChain: [...this.runtime.packageResolutionChain],
      ...(this.runtime.selfTest
        ? { selfTest: { ...this.runtime.selfTest } }
        : {}),
    };
  }

  private createRunner(
    Runner: BusyModule['BusyTexRunner'],
    engine: LearnitiveCompiler,
  ): BusyRunner {
    const basePath =
      this.options.basePath ??
      this.deploymentStatus?.runtimeAssets?.busytex?.basePath ??
      DEFAULT_BASE_PATH;
    const basic = `${basePath}/texlive-basic.js`;
    const recommended = `${basePath}/texlive-recommended.js`;
    const extra = `${basePath}/texlive-extra.js`;
    // Build Fix 50.6: BusyTeX resolves direct \usepackage declarations by
    // inspecting the JS package catalogs before it starts TeX. Keep all three
    // shipped TeX Live catalogs visible so packages such as font collections
    // can lazily mount texlive-extra instead of falling through to a remote
    // lookup even though the full-profile asset is already deployed.
    // The .data payload remains lazy; this does not preload texlive-extra.
    // BusyTeX's remote TeX Live resolver is incremental, not a bootstrap
    // replacement. kpathsea still needs texmf.cnf and the prebuilt engine
    // format from a mounted base bundle before it can resolve anything from
    // TEXLIVE_REMOTE_ENDPOINT. In particular, pdfLaTeX must never initialize
    // with an empty preload list: without texlive-basic the runtime falls back
    // to mktexfmt, which cannot fork inside the browser WASM environment.
    const preloadDataPackages =
      engine === 'pdflatex' ? [basic, recommended] : [recommended];
    return new Runner({
      busytexBasePath: basePath,
      verbose: false,
      engineMode: 'combined',
      preloadDataPackages,
      catalogDataPackages: [basic, recommended, extra],
      onDownloadProgress: ({ percent }) => {
        if (this.packageTelemetry) {
          const now = performance.now();
          this.packageTelemetry.startedAt ??= now;
          this.packageTelemetry.lastAt = now;
          this.packageTelemetry.events += 1;
        }
        this.progressListener?.({
          phase: 'downloading-packages',
          ...(typeof percent === 'number' ? { percent } : {}),
          message: 'Downloading missing TeX Live files',
        });
      },
    });
  }

  private async initializeRunner(
    Runner: BusyModule['BusyTexRunner'],
    engine: LearnitiveCompiler,
    runtimeAbi: BusyTexRuntimeAbi,
    runtimeBasePath: string,
    signal?: AbortSignal,
  ): Promise<BusyRunner> {
    const runner = this.createRunner(Runner, engine);
    await initializeBusyTexWithWorkerBridge(
      runner,
      runtimeBasePath,
      runtimeAbi,
      engine,
      signal,
    );
    signal?.throwIfAborted();
    const restored = await restoreRemoteCache(
      runner,
      this.deploymentStatus?.runtimeAssets?.busytex?.version ?? undefined,
    );
    this.restoredRemoteNames.clear();
    for (const name of restored.restoredNames) this.restoredRemoteNames.add(name);
    return runner;
  }

  private clearRunner(): void {
    this.runner?.terminate();
    this.runner = undefined;
    this.initializationMode = undefined;
    this.initializedWithRecommended = false;
    this.runtimeAbi = undefined;
    this.restoredRemoteNames.clear();
  }

  private async initializeForEngine(
    engine: LearnitiveCompiler,
    signal?: AbortSignal,
  ): Promise<void> {
    signal?.throwIfAborted();
    if (!busyTexAdapterReleaseLogged) {
      console.info(`[BusyTeX] Learnitive compiler adapter ${LEARNITIVE_BUSYTEXT_ADAPTER_RELEASE} active`);
      busyTexAdapterReleaseLogged = true;
    }
    if (
      this.runner?.isInitialized() &&
      (engine === 'pdflatex' || this.initializedWithRecommended)
    ) {
      return;
    }
    if (this.loading) {
      await this.loading;
      if (
        this.runner?.isInitialized() &&
        (engine === 'pdflatex' || this.initializedWithRecommended)
      ) {
        return;
      }
    }

    this.loading = (async () => {
      if (this.runner?.isInitialized()) {
        this.clearRunner();
      }
      const status = await loadCompilerDeploymentStatus(signal);
      this.deploymentStatus = status;
      const assets = status.runtimeAssets?.busytex;
      this.runtime = {
        status: 'initializing',
        backend: 'busytex',
        serviceWorker: 'unknown',
        packageResolutionChain: [
          'deployed BusyTeX asset preflight',
          'BusyTeX runtime assets',
          engine === 'pdflatex'
            ? 'TeX Live basic + recommended runtime (formats, TS1/cm-super fonts/maps)'
            : 'TeX Live recommended preload for XeLaTeX/LuaLaTeX',
          'persistent browser package cache',
          'TeX Live full-tree package + font endpoint',
        ],
        ...(assets?.version ? { assetVersion: assets.version } : {}),
        ...(assets?.fingerprint
          ? { assetFingerprint: assets.fingerprint }
          : {}),
      };

      try {
        const capability = status.engines[engine];
        if (!capability.available) {
          throw new Error(
            capability.reason ?? `${engine} is not enabled by this deployment`,
          );
        }
        await assertBusyTexDeploymentAssets(status, engine, signal);
        const runtimeBasePath =
          this.options.basePath ?? assets?.basePath ?? DEFAULT_BASE_PATH;
        const runtimeAbi = await inspectBusyTexRuntimeAbi(runtimeBasePath, signal);
        this.runtimeAbi = runtimeAbi;

        const controlled = await ensureTexliveServiceWorker();
        const serviceWorkerAvailable =
          typeof navigator !== 'undefined' && 'serviceWorker' in navigator;
        this.runtime = {
          ...this.runtime,
          serviceWorker: controlled
            ? 'controlled'
            : serviceWorkerAvailable
              ? 'fallback-preload'
              : 'unsupported',
        };

        const moduleUrl = versionedRuntimeUrl(
          this.options.moduleUrl ?? assets?.moduleUrl ?? DEFAULT_MODULE_URL,
          assets?.fingerprint,
        );
        const module = await importBusyModule(moduleUrl);
        signal?.throwIfAborted();
        const Runner = module.BusyTexRunner;
        if (typeof Runner !== 'function') {
          throw new Error('BusyTeX runtime did not expose BusyTexRunner');
        }
        const runner = await this.initializeRunner(
          Runner,
          engine,
          runtimeAbi,
          runtimeBasePath,
          signal,
        );
        this.initializationMode = 'worker';

        this.runner = runner;
        this.initializedWithRecommended = true;
        this.runtime = {
          ...this.runtime,
          status: 'ready',
          ...(this.initializationMode
            ? { initializationMode: this.initializationMode }
            : {}),
        };
      } catch (error) {
        signal?.throwIfAborted();
        const message = errorChain(error);
        this.runtime = { ...this.runtime, status: 'error', error: message };
        this.clearRunner();
        throw error;
      }
    })();

    try {
      await this.loading;
    } finally {
      this.loading = undefined;
    }
  }

  private isEngineReady(engine: LearnitiveCompiler): boolean {
    return Boolean(
      this.runner?.isInitialized() &&
        (engine === 'pdflatex' || this.initializedWithRecommended),
    );
  }

  private async ensureEngineReady(
    engine: LearnitiveCompiler,
    signal?: AbortSignal,
  ): Promise<BusyRunner> {
    await this.initializeForEngine(engine, signal);
    const runner = this.runner;
    if (!runner) throw new Error('BusyTeX failed to initialize');
    return runner;
  }

  async initialize(signal?: AbortSignal): Promise<void> {
    await this.initializeForEngine('pdflatex', signal);
  }

  private async executeCompile(
    runner: BusyRunner,
    request: LatexCompileRequest,
    texliveEndpoint: string,
  ): Promise<BusyResult> {
    const plan = bibliographyPlanFor(request);
    const driver = BUSYTEX_DRIVER_BY_COMPILER[request.compiler];
    if (plan.biberRequested && this.runtimeAbi?.supportsBiber === false) {
      throw new Error(
        'This deployment is using the Learnitive BusyTeX legacy compatibility shim. PDF/Xe/Lua compilation and SyncTeX are available, but Biber requires the native BusyTeX 1.4 pipeline. Use BibTeX or install the native v1.4 runtime before compiling this biblatex document.',
      );
    }

    // Learnitive always calls the public 1.4 runner API. The isolated Learnitive
    // worker bridge adapts that named worker message to the detected pipeline
    // ABI, so legacy no-Biber runtimes never receive a shifted driver slot.
    const compileDirect = () =>
      runner.compile(
        cloneFiles(request.files),
        request.rootFile,
        plan.bibtex,
        plan.biberRequested,
        plan.makeindex,
        plan.rerun,
        'silent',
        driver,
        null,
        texliveEndpoint,
        false,
        [],
      );

    return abortable(
      compileDirect(),
      request.signal,
      () => {
        runner.terminate();
        if (this.runner === runner) this.clearRunner();
      },
    );
  }

  async compile(request: LatexCompileRequest): Promise<LatexCompileResult> {
    request.signal?.throwIfAborted();
    this.progressListener = request.onProgress;
    this.packageTelemetry = { events: 0 };
    const totalStarted = performance.now();
    const runtimeWasWarm = this.isEngineReady(request.compiler);
    let runtimeInitMs = 0;
    let engineCompileMs = 0;
    request.onProgress?.({
      phase: 'initializing',
      message: `Starting BusyTeX ${request.compiler}`,
    });

    try {
      const initStarted = performance.now();
      let runner = await this.ensureEngineReady(request.compiler, request.signal);
      if (!runtimeWasWarm) runtimeInitMs += Math.max(0, performance.now() - initStarted);
      const deploymentStatus =
        this.deploymentStatus ?? (await loadCompilerDeploymentStatus(request.signal));
      const texliveEndpoint =
        this.options.texliveEndpoint ??
        deploymentStatus.texlive.busytexEndpoint ??
        DEFAULT_TEXLIVE_ENDPOINT;
      const plan = bibliographyPlanFor(request);
      const projectFileNames = new Set(
        request.files
          .map((file) => file.path.replace(/\\/gu, '/'))
          .map((path) => path.slice(path.lastIndexOf('/') + 1))
          .filter(Boolean),
      );
      request.onProgress?.({
        phase: 'mounting-files',
        message: 'Mounting project files',
      });
      request.signal?.throwIfAborted();
      request.onProgress?.({
        phase: 'compiling',
        message: `Compiling with ${request.compiler}`,
      });

      let result: BusyResult;
      const executeTimed = async () => {
        const started = performance.now();
        try {
          return await this.executeCompile(runner, request, texliveEndpoint);
        } finally {
          engineCompileMs += Math.max(0, performance.now() - started);
        }
      };
      try {
        result = await executeTimed();
      } catch (firstWorkerError) {
        request.signal?.throwIfAborted();
        request.onProgress?.({
          phase: 'initializing',
          message: `BusyTeX worker stopped unexpectedly; restarting the isolated worker for ${request.compiler}`,
        });
        this.clearRunner();
        const retryInitStarted = performance.now();
        await this.initializeForEngine(request.compiler, request.signal);
        runtimeInitMs += Math.max(0, performance.now() - retryInitStarted);
        runner = this.runner as BusyRunner;
        try {
          result = await executeTimed();
        } catch (secondWorkerError) {
          throw new Error(
            `BusyTeX ${request.compiler} compilation failed after an isolated-worker restart. First worker: ${errorChain(firstWorkerError)}. Second worker: ${errorChain(secondWorkerError)}`,
            { cause: secondWorkerError },
          );
        }
      }

      request.signal?.throwIfAborted();
      const missingDependency =
        (!result.success || result.exitCode !== 0) && texliveEndpoint
          ? missingTexliveDependency(result.log, projectFileNames)
          : undefined;
      if (missingDependency) {
        // BF50.7 full-tree resilience: a stale negative kpse lookup can survive
        // the first pass even though the configured endpoint contains the file.
        // Clear only BusyTeX's remote miss set and retry the same isolated
        // browser runtime once. Project files are never altered or substituted.
        request.onProgress?.({
          phase: 'downloading-packages',
          message: `Retrying with TeX Live full coverage for ${missingDependency}`,
        });
        await runner.writeTexliveRemoteMisses?.([]);
        result = await executeTimed();
      }

      request.signal?.throwIfAborted();
      const pdfReadyStarted = performance.now();
      const packageTelemetry = this.packageTelemetry;
      // A warm compile with no package-download activity has nothing new to
      // persist. Avoid a redundant MEMFS remote-cache scan on every Live PDF
      // refresh; real package downloads still persist before the result returns.
      const cacheResult = packageTelemetry?.events
        ? await persistRemoteCache(
            runner,
            deploymentStatus.runtimeAssets?.busytex?.version ?? undefined,
            { projectFileNames },
          )
        : { persistedNames: [], invalidNames: [], scopeLeakDetected: false };
      await runner.writeTexliveRemoteMisses?.([]);
      if (cacheResult.scopeLeakDetected) {
        console.warn(
          '[BusyTeX] Remote package-cache persistence skipped because this runtime returned project-local files for /tmp/texlive_remote.',
        );
      }
      if (cacheResult.invalidNames.length > 0) {
        console.warn(
          `[BusyTeX] Discarded invalid TeX Live remote-cache entries without affecting the compile result: ${cacheResult.invalidNames.join(', ')}`,
        );
      }
      const packagesFetched = cacheResult.persistedNames.filter(
        (name) => !this.restoredRemoteNames.has(name),
      ).length;
      for (const name of cacheResult.persistedNames) this.restoredRemoteNames.add(name);

      request.onProgress?.({ phase: 'finalizing', percent: 100 });
      const generatedArtifactCollection = request.artifactReadback === 'none'
        ? {
            artifacts: [],
            readback: { attempts: [], mergedFileCount: 0 },
          }
        : await collectGeneratedArtifacts(runner, request.files);
      const pdfReadyMs = Math.max(0, performance.now() - pdfReadyStarted);
      const totalMs = Math.max(0, performance.now() - totalStarted);
      const packageFetchMs =
        packageTelemetry?.startedAt !== undefined && packageTelemetry.lastAt !== undefined
          ? Math.max(0, packageTelemetry.lastAt - packageTelemetry.startedAt)
          : 0;

      return {
        ...buildLatexCompileResult({
          backend: this.backend,
          compiler: request.compiler,
          rootFile: request.rootFile,
          compileMode: request.mode ?? 'full',
          exitCode: result.exitCode,
          reportedSuccess: result.success,
          ...(result.pdf ? { pdf: result.pdf } : {}),
          ...(result.synctex ? { synctex: result.synctex } : {}),
          log: result.log,
          durationMs: totalMs,
          bibliography: plan,
          artifacts: generatedArtifactCollection.artifacts,
          performance: {
            runtimeInitMs,
            packageFetchMs,
            // BusyTeX currently exposes bibliography planning but not a
            // separately timed bibliography callback, so that work remains
            // included in engineCompileMs until the runtime exposes one.
            bibliographyMs: 0,
            engineCompileMs,
            pdfReadyMs,
            totalMs,
            runtimeWasWarm,
            packagesFetched,
          },
        }),
        runtimeDiagnostics: {
          ...this.getRuntimeDiagnostics(),
          artifactReadback: generatedArtifactCollection.readback,
        },
      };
    } catch (error) {
      const message = errorChain(error);
      this.runtime = { ...this.runtime, status: 'error', error: message };
      throw new Error(`BusyTeX ${request.compiler} runtime failed: ${message}`, {
        cause: error,
      });
    } finally {
      this.progressListener = undefined;
      this.packageTelemetry = undefined;
    }
  }

  cancel(): void {
    this.clearRunner();
    if (this.runtime.status !== 'error') {
      this.runtime = { ...this.runtime, status: 'idle' };
    }
  }

  dispose(): void {
    this.cancel();
    this.deploymentStatus = undefined;
  }
}
