import { BaseService, type BaseServiceContext } from '@learnitive/base-utils';
import { SERVICE_NAME } from '@learnitive/constants';
import {
  buildLatexCompileResult,
  collectProjectCompileFiles,
  type LatexCompileProgress,
  type LatexCompileResult,
  type LatexCompiler,
  type LatexCompilerCapabilities,
  type LatexCompilerRuntimeDiagnostics,
} from '@learnitive/latex-compiler';
import { atom } from 'jotai';
import type { LearnitiveCompiler, LearnitiveCompileTarget, LearnitivePdfCompileMode } from '@learnitive/project-storage';
import type { LatexSourceService } from './latex-source-service';
import type { ProjectStorageService } from './project-storage-service';
import type { ProjectCollaborationService } from './project-collaboration-service';
import type { ProjectEntitlementService } from './project-entitlement-service';
import {
  clearSessionCloudFallback,
  hasSessionCloudFallback,
  rememberSessionCloudFallback,
} from './project-cloud-fallback-session';

export type ProjectCompileStatus =
  | 'idle'
  | 'initializing'
  | 'compiling'
  | 'success'
  | 'error'
  | 'cancelled';


export type ProjectCompileState = {
  status: ProjectCompileStatus;
  wsName?: string;
  rootFile?: string;
  progress?: LatexCompileProgress;
  result?: LatexCompileResult;
  runtimeDiagnostics?: LatexCompilerRuntimeDiagnostics;
  error?: string;
};

export type ProjectCompilerCapabilitiesState = {
  status: 'idle' | 'loading' | 'ready' | 'error';
  capabilities?: LatexCompilerCapabilities;
  runtimeDiagnostics?: LatexCompilerRuntimeDiagnostics;
  error?: string;
};

type Config = {
  createCompiler: () => LatexCompiler;
};

const FULL_TEXLIVE_FALLBACK_PATTERNS: readonly RegExp[] = [
  /File [`'][^`'\r\n]+\.(?:sty|cls|clo|cfg|def|fd|bbx|cbx|lbx|bst|enc|map|tfm|vf|pfb|otf|ttf|lua)[`'] not found/iu,
  /I can't find file [`'][^`'\r\n]+\.(?:sty|cls|clo|cfg|def|fd|bbx|cbx|lbx|bst|enc|map|tfm|vf|pfb|otf|ttf|lua)[`']/iu,
  /Package fontspec Error:.*font.*cannot be found/isu,
  /(?:^|\n)!?[^\r\n]{0,160}\bfont\b[^\r\n]{0,320}\b(?:not found|not loadable|cannot be found|could not be found)\b/imu,
  /(?:^|\n)(?:luaotfload|kpathsea|mktextfm|mktexpk)[^\r\n]{0,500}(?:not found|failed|nothing appropriate|cannot find)/imu,
  /(?:TFM|metric)\s+(?:file|data)[^\r\n]{0,240}(?:not found|missing|unavailable)/iu,
  /I can't find the format file/iu,
  /Fatal format file error/iu,
  /TeX capacity exceeded/iu,
  /(?:biber|bibtex|makeindex).*?(?:not found|unavailable|failed to (?:start|load))/iu,
  /BusyTeX runtime failed/iu,
  /requires (?:the )?BusyTeX browser compiler/iu,
  /Full \/ SyncTeX .*BusyTeX/iu,
  /TeX Live .*?(?:unavailable|failed|could not|returned HTML)/iu,
  /WebAssembly.*?(?:failed|trap|unavailable)/iu,
];

export function fullTexliveFallbackReasonFromText(text: string): string | undefined {
  const missing = text.match(/File [`']([^`'\r\n]+\.(?:sty|cls|clo|cfg|def|fd|bbx|cbx|lbx|bst|enc|map|tfm|vf|pfb|otf|ttf|lua))[`'] not found/iu)?.[1];
  if (missing) return `missing browser TeX Live dependency ${missing}`;
  if (FULL_TEXLIVE_FALLBACK_PATTERNS.some((pattern) => pattern.test(text))) {
    return 'browser TeX Live package, font, bibliography or runtime limitation';
  }
  return undefined;
}

function fullTexliveFallbackReason(result: LatexCompileResult): string | undefined {
  if (result.success) return undefined;
  return fullTexliveFallbackReasonFromText(
    [
      result.log,
      ...result.diagnostics.map((diagnostic) => diagnostic.message),
      result.runtimeDiagnostics?.error,
    ]
      .filter(Boolean)
      .join('\n'),
  );
}

/**
 * Coordinates a durable project snapshot with the browser compiler. Generated
 * PDF/SyncTeX/log outputs remain runtime artifacts, not project files, so a
 * compile never dirties Git or overwrites user data.
 */
export class ProjectCompileService extends BaseService {
  static deps = ['latexSource', 'projectStorage', 'projectCollaboration', 'projectEntitlements'] as const;

  readonly $state = atom<ProjectCompileState>({ status: 'idle' });
  readonly $capabilities = atom<ProjectCompilerCapabilitiesState>({ status: 'idle' });

  private compiler: LatexCompiler | undefined;
  private capabilitiesLoading: Promise<LatexCompilerCapabilities> | undefined;
  private running: AbortController | undefined;
  private readonly lastSuccessful = new Map<string, LatexCompileResult>();

  constructor(
    context: BaseServiceContext,
    private readonly dependencies: {
      latexSource: LatexSourceService;
      projectStorage: ProjectStorageService;
      projectCollaboration: ProjectCollaborationService;
      projectEntitlements: ProjectEntitlementService;
    },
    private readonly config: Config,
  ) {
    super(SERVICE_NAME.projectCompileService, context, dependencies);
  }

  hookMount(): void {
    this.addCleanup(() => {
      this.cancel();
      this.compiler?.dispose();
      this.compiler = undefined;
      this.capabilitiesLoading = undefined;
      this.store.set(this.$capabilities, { status: 'idle' });
      this.lastSuccessful.clear();
    });
  }

  async getCompilerCapabilities(
    signal?: AbortSignal,
  ): Promise<LatexCompilerCapabilities> {
    await this.mountPromise;
    signal?.throwIfAborted();
    const current = this.store.get(this.$capabilities);
    if (current.status === 'ready' && current.capabilities) {
      return current.capabilities;
    }
    if (this.capabilitiesLoading) return this.capabilitiesLoading;

    this.store.set(this.$capabilities, { status: 'loading' });
    let compiler = this.compiler;
    if (!compiler) {
      compiler = this.config.createCompiler();
      this.compiler = compiler;
    }
    const loading = (async () => {
      if (compiler.getCapabilities) {
        // R14.24.1: capability reads must stay metadata-only. Runtime loading,
        // package preloads and compiler execution happen only for an explicit
        // user compile; release/runtime smoke tests live in certification, not
        // in the user's ordinary project path.
        return compiler.getCapabilities(signal);
      }
      const available = (engine: LearnitiveCompiler) => compiler.supports(engine);
      return {
        schemaVersion: 1 as const,
        mode: available('xelatex') || available('lualatex')
          ? ('busytex-with-swiftlatex-fallback' as const)
          : available('pdflatex')
            ? ('swiftlatex-pdflatex' as const)
            : ('unavailable' as const),
        engines: {
          pdflatex: {
            available: available('pdflatex'),
            preferredBackend: available('pdflatex') ? compiler.backend : null,
            reason: available('pdflatex') ? null : 'pdfLaTeX is unavailable.',
            supportsSyncTeX: compiler.backend === 'busytex' && available('pdflatex'),
            syncTeXBackend:
              compiler.backend === 'busytex' && available('pdflatex') ? 'busytex' : null,
          },
          xelatex: {
            available: available('xelatex'),
            preferredBackend: available('xelatex') ? compiler.backend : null,
            reason: available('xelatex') ? null : 'XeLaTeX is unavailable.',
            supportsSyncTeX: compiler.backend === 'busytex' && available('xelatex'),
            syncTeXBackend:
              compiler.backend === 'busytex' && available('xelatex') ? 'busytex' : null,
          },
          lualatex: {
            available: available('lualatex'),
            preferredBackend: available('lualatex') ? compiler.backend : null,
            reason: available('lualatex') ? null : 'LuaLaTeX is unavailable.',
            supportsSyncTeX: compiler.backend === 'busytex' && available('lualatex'),
            syncTeXBackend:
              compiler.backend === 'busytex' && available('lualatex') ? 'busytex' : null,
          },
        },
        texlive: {
          bundled: {
            available: false,
            bundleId: null,
            compatibility: null,
            fileCount: 0,
            textFileCount: 0,
            binaryFileCount: 0,
          },
          staticMirror: { enabled: false, endpoint: null, index: null },
          remote: { enabled: false, endpoint: null, configurable: true },
          busytexEndpoint: null,
        },
      } satisfies LatexCompilerCapabilities;
    })();
    this.capabilitiesLoading = loading;

    try {
      const capabilities = await loading;
      this.store.set(this.$capabilities, {
        status: 'ready',
        capabilities,
        ...(compiler.getRuntimeDiagnostics
          ? { runtimeDiagnostics: compiler.getRuntimeDiagnostics() }
          : {}),
      });
      return capabilities;
    } catch (error) {
      const message =
        error instanceof Error ? error.message : 'Compiler capability detection failed';
      this.store.set(this.$capabilities, {
        status: 'error',
        error: message,
        ...(compiler.getRuntimeDiagnostics
          ? { runtimeDiagnostics: compiler.getRuntimeDiagnostics() }
          : {}),
      });
      throw error;
    } finally {
      if (this.capabilitiesLoading === loading) {
        this.capabilitiesLoading = undefined;
      }
    }
  }

  async getProjectCompileTarget(wsName: string): Promise<LearnitiveCompileTarget> {
    return (await this.dependencies.projectStorage.getProjectConfig(wsName)).compileTarget ?? 'local';
  }

  async getProjectCloudFallbackSticky(wsName: string): Promise<boolean> {
    if (hasSessionCloudFallback(wsName)) return true;
    return (await this.dependencies.projectStorage.getProjectConfig(wsName)).cloudFallbackSticky === true;
  }

  private async rememberCloudFallback(wsName: string): Promise<boolean> {
    rememberSessionCloudFallback(wsName);
    try {
      await this.dependencies.projectStorage.updateProjectConfig(wsName, (current) => ({
        ...current,
        cloudFallbackSticky: true,
      }));
      return true;
    } catch {
      // Session memory deliberately remains sticky even if durable metadata
      // cannot be written. This keeps Compile and Live PDF on the proven cloud
      // path for the rest of the current Learnitive session.
      return true;
    }
  }

  async setProjectCompileTarget(wsName: string, target: LearnitiveCompileTarget): Promise<void> {
    if (target === 'cloud') {
      const capabilities = await this.dependencies.projectEntitlements.cloudCompileCapabilities();
      if (!capabilities.enabled) throw new Error('Cloud Compile is disabled on this Learnitive deployment.');
    }
    await this.dependencies.projectStorage.updateProjectConfig(wsName, (current) => ({
      ...current,
      compileTarget: target,
      // Explicit target selection is the user's reset switch. Choosing Local
      // retries the browser path; choosing Cloud makes the automatic
      // fallback preference irrelevant.
      cloudFallbackSticky: false,
    }));
    clearSessionCloudFallback(wsName);
  }

  getCloudCompilerCapabilities() {
    return this.dependencies.projectEntitlements.cloudCompileCapabilities();
  }

  async getProjectCompiler(wsName: string): Promise<LearnitiveCompiler> {
    return (await this.dependencies.projectStorage.getProjectConfig(wsName)).compiler;
  }

  async setProjectCompiler(
    wsName: string,
    nextCompiler: LearnitiveCompiler,
  ): Promise<void> {
    const projectConfig = await this.dependencies.projectStorage.getProjectConfig(wsName);
    const target = projectConfig.compileTarget ?? 'local';
    if (target === 'cloud') {
      const capabilities = await this.getCloudCompilerCapabilities();
      if (!capabilities.enabled || !capabilities.allowedEngines.includes(nextCompiler)) {
        throw new Error(`${nextCompiler} is unavailable for Cloud Compile in this Learnitive deployment`);
      }
    } else if (projectConfig.cloudFallbackSticky === true || hasSessionCloudFallback(wsName)) {
      const cloud = await this.getCloudCompilerCapabilities();
      if (!cloud.freeFallbackEnabled || !cloud.allowedEngines.includes(nextCompiler)) {
        throw new Error(`${nextCompiler} is unavailable for the remembered Docker TeX Live Full fallback`);
      }
    } else {
      const capabilities = await this.getCompilerCapabilities();
      const capability = capabilities.engines[nextCompiler];
      if (!capability.available) {
        throw new Error(
          capability.reason ?? `${nextCompiler} is unavailable in this Learnitive deployment`,
        );
      }
    }
    await this.dependencies.projectStorage.updateProjectConfig(wsName, (current) => ({
      ...current,
      compiler: nextCompiler,
      // Changing engines is an explicit request to reconsider the browser
      // path on the next compile.
      cloudFallbackSticky: false,
    }));
    clearSessionCloudFallback(wsName);
  }


  async getProjectPdfCompileMode(wsName: string): Promise<LearnitivePdfCompileMode> {
    return (await this.dependencies.projectStorage.getProjectConfig(wsName)).pdfCompileMode ?? 'full';
  }

  async setProjectPdfCompileMode(
    wsName: string,
    mode: LearnitivePdfCompileMode,
  ): Promise<void> {
    // Full / SyncTeX is a user intent, not a browser-capability privilege.
    // Persist it even when BusyTeX is unavailable: compile() will route to the
    // isolated server TeX Live fallback and let that authoritative runtime
    // decide whether the requested engine can execute.
    await this.dependencies.projectStorage.updateProjectConfig(wsName, (current) => ({
      ...current,
      pdfCompileMode: mode,
      // Changing preview mode explicitly retries the browser compiler.
      cloudFallbackSticky: false,
    }));
    clearSessionCloudFallback(wsName);
  }

  getLastSuccessful(wsName: string): LatexCompileResult | undefined {
    return this.lastSuccessful.get(wsName);
  }

  async compile(wsName: string, signal?: AbortSignal): Promise<LatexCompileResult> {
    return this.executeCompile(wsName, signal);
  }

  private async executeCompile(
    wsName: string,
    signal: AbortSignal | undefined,
  ): Promise<LatexCompileResult> {
    await this.mountPromise;
    if (this.running) this.cancel();

    const controller = new AbortController();
    this.running = controller;
    const relayAbort = () => controller.abort(signal?.reason);
    signal?.addEventListener('abort', relayAbort, { once: true });

    try {
      this.store.set(this.$state, {
        status: 'initializing',
        wsName,
        progress: { phase: 'initializing' },
      });

      await this.dependencies.projectCollaboration.flushToStorage(wsName);
      await this.dependencies.latexSource.flushWorkspace(wsName);
      if (this.dependencies.latexSource.hasPendingOrFailedSaveInWorkspace(wsName)) {
        throw new Error(
          'Compilation blocked because a LaTeX source file could not be saved safely',
        );
      }
      controller.signal.throwIfAborted();

      const [projectConfig, storage] = await Promise.all([
        this.dependencies.projectStorage.getProjectConfig(wsName),
        this.dependencies.projectStorage.getProvider(wsName),
      ]);
      const snapshot = await collectProjectCompileFiles(
        storage,
        projectConfig.rootFile,
        controller.signal,
      );
      controller.signal.throwIfAborted();

      const commitResult = (result: LatexCompileResult): LatexCompileResult => {
        if (result.success) this.lastSuccessful.set(wsName, result);
        this.store.set(this.$state, {
          status: result.success ? 'success' : 'error',
          wsName,
          rootFile: snapshot.rootFile,
          result,
          ...(result.runtimeDiagnostics ? { runtimeDiagnostics: result.runtimeDiagnostics } : {}),
          ...(result.success
            ? {}
            : {
                error:
                  result.diagnostics.find((item) => item.severity === 'error')?.message ??
                  'LaTeX compilation failed',
              }),
        });
        return result;
      };

      const runCloudCompile = async (fallback: boolean, reason?: string): Promise<LatexCompileResult> => {
        const startedAt = performance.now();
        this.store.set(this.$state, {
          status: 'compiling',
          wsName,
          rootFile: snapshot.rootFile,
          progress: {
            phase: 'compiling',
            message: fallback
              ? 'Retrying with Docker TeX Live Full fallback'
              : 'Compiling with Learnitive Cloud Compile',
          },
        });
        const cloud = await this.dependencies.projectEntitlements.cloudCompile({
          engine: projectConfig.compiler,
          rootFile: snapshot.rootFile,
          files: snapshot.files,
          signal: controller.signal,
          ...(fallback ? { fallback: true } : {}),
        });
        controller.signal.throwIfAborted();
        const cloudSucceeded = cloud.success === true && Boolean(cloud.pdf?.byteLength);
        const remembered = fallback && cloudSucceeded
          ? projectConfig.cloudFallbackSticky === true || await this.rememberCloudFallback(wsName)
          : false;
        const fallbackHeader = fallback
          ? [
              '===== Learnitive Full TeX Live fallback =====',
              `Compilation is running in the isolated Docker TeX Live Full environment${reason ? ` (${reason})` : ''}.`,
              cloudSucceeded
                ? remembered
                  ? 'Learnitive remembered this fallback for the project; future Compile and Live PDF updates will use Docker TeX Live Full directly.'
                  : 'This compile used Docker TeX Live Full, but the project preference could not be retained.'
                : 'Docker TeX Live Full did not produce a PDF, so the project fallback preference was not changed.',
              'This reliability fallback is available to Free and Pro users.',
              '==========================================',
              '',
            ].join('\n')
          : '';
        return buildLatexCompileResult({
          backend: 'cloud',
          compiler: cloud.effectiveEngine ?? projectConfig.compiler,
          compileMode: 'full',
          rootFile: snapshot.rootFile,
          exitCode: cloud.success ? 0 : 1,
          reportedSuccess: cloud.success,
          ...(cloud.pdf ? { pdf: cloud.pdf } : {}),
          ...(cloud.synctex ? { synctex: cloud.synctex } : {}),
          log: `${fallbackHeader}${cloud.log}`,
          durationMs: Math.max(0, performance.now() - startedAt),
          bibliography: { bibtex: false, biberRequested: false, makeindex: false, rerun: false },
        });
      };

      const compileMode =
        projectConfig.compiler === 'pdflatex'
          ? projectConfig.pdfCompileMode ?? 'full'
          : 'full';
      const wantsFullTexLive = compileMode === 'full';

      if ((projectConfig.compileTarget ?? 'local') === 'cloud') {
        return commitResult(await runCloudCompile(false));
      }

      if (projectConfig.cloudFallbackSticky === true || hasSessionCloudFallback(wsName)) {
        // A remembered project goes directly to Docker on subsequent compiles.
        // The cloud endpoint remains the authority if deployment policy later
        // changes; retrying a browser runtime already known to be insufficient
        // would only add latency before surfacing that server-side error.
        return commitResult(await runCloudCompile(true, 'remembered for this project'));
      }

      let compiler: LatexCompiler;
      let capabilities: LatexCompilerCapabilities;
      try {
        let currentCompiler = this.compiler;
        if (!currentCompiler) {
          currentCompiler = this.config.createCompiler();
          this.compiler = currentCompiler;
        }
        compiler = currentCompiler;
        capabilities = await this.getCompilerCapabilities(controller.signal);
      } catch (browserCapabilityError) {
        controller.signal.throwIfAborted();
        if (wantsFullTexLive) {
          try {
            return commitResult(await runCloudCompile(true, 'browser compiler capability initialization failed'));
          } catch (fallbackError) {
            controller.signal.throwIfAborted();
            const browserMessage = browserCapabilityError instanceof Error ? browserCapabilityError.message : 'Browser compiler capability initialization failed';
            const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : 'Full TeX Live fallback failed';
            throw new Error(`${browserMessage} Isolated Full TeX Live fallback also failed: ${fallbackMessage}`);
          }
        }
        throw browserCapabilityError;
      }
      const capability = capabilities.engines[projectConfig.compiler];
      if (!capability?.available || !compiler.supports(projectConfig.compiler)) {
        const localReason =
          capability.reason ?? `No installed browser compiler supports ${projectConfig.compiler}`;
        if (wantsFullTexLive) {
          try {
            return commitResult(await runCloudCompile(true, 'selected engine is unavailable in the browser runtime'));
          } catch (fallbackError) {
            controller.signal.throwIfAborted();
            const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : 'Full TeX Live fallback failed';
            throw new Error(`${localReason} Isolated Full TeX Live fallback also failed: ${fallbackMessage}`);
          }
        }
        throw new Error(localReason);
      }

      // Fast Preview may use the lightweight browser pdfLaTeX path. Full /
      // SyncTeX requires real SyncTeX-capable compilation. When the browser
      // profile cannot provide that, go straight to the staging Universal
      // Runner instead of invoking a browser compiler that can only return the
      // "BusyTeX full profile required" error seen by users.
      if (wantsFullTexLive && !capability.supportsSyncTeX) {
        try {
          return commitResult(await runCloudCompile(true, 'BusyTeX Full / SyncTeX capability is unavailable in the browser'));
        } catch (fallbackError) {
          controller.signal.throwIfAborted();
          const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : 'Full TeX Live fallback failed';
          throw new Error(`Full / SyncTeX requires a full compiler. Isolated Full TeX Live fallback failed: ${fallbackMessage}`);
        }
      }

      this.store.set(this.$state, {
        status: 'compiling',
        wsName,
        rootFile: snapshot.rootFile,
        progress: { phase: 'initializing' },
        ...(compiler.getRuntimeDiagnostics
          ? { runtimeDiagnostics: compiler.getRuntimeDiagnostics() }
          : {}),
      });

      let result: LatexCompileResult;
      try {
        result = await compiler.compile({
          compiler: projectConfig.compiler,
          mode: compileMode,
          rootFile: snapshot.rootFile,
          files: snapshot.files,
          signal: controller.signal,
          onProgress: (progress) => {
            if (this.running !== controller || controller.signal.aborted) return;
            const current = this.store.get(this.$state);
            this.store.set(this.$state, {
              ...current,
              status: 'compiling',
              wsName,
              rootFile: snapshot.rootFile,
              progress,
              ...(compiler.getRuntimeDiagnostics
                ? { runtimeDiagnostics: compiler.getRuntimeDiagnostics() }
                : {}),
            });
          },
        });
      } catch (localRuntimeError) {
        controller.signal.throwIfAborted();
        if (wantsFullTexLive) {
          try {
            return commitResult(await runCloudCompile(true, 'browser compiler runtime failure'));
          } catch (fallbackError) {
            controller.signal.throwIfAborted();
            const localMessage = localRuntimeError instanceof Error ? localRuntimeError.message : 'Browser compiler failed';
            const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : 'Full TeX Live fallback failed';
            throw new Error(`${localMessage} Isolated Full TeX Live fallback also failed: ${fallbackMessage}`);
          }
        }
        throw localRuntimeError;
      }
      controller.signal.throwIfAborted();

      const fallbackReason = fullTexliveFallbackReason(result);
      if (wantsFullTexLive && fallbackReason) {
        try {
          return commitResult(await runCloudCompile(true, fallbackReason));
        } catch (fallbackError) {
          controller.signal.throwIfAborted();
          const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : 'Full TeX Live fallback failed';
          const localMessage = result.diagnostics.find((item) => item.severity === 'error')?.message || fallbackReason;
          throw new Error(`${localMessage} Isolated Full TeX Live fallback also failed: ${fallbackMessage}`);
        }
      }

      const runtimeDiagnostics = result.runtimeDiagnostics ?? compiler.getRuntimeDiagnostics?.();
      const finalized: LatexCompileResult = {
        ...result,
        ...(runtimeDiagnostics ? { runtimeDiagnostics } : {}),
      };
      return commitResult(finalized);
    } catch (error) {
      const cancelled = controller.signal.aborted;
      const message =
        error instanceof Error ? error.message : 'LaTeX compilation failed';
      if (this.running === controller) {
        this.store.set(this.$state, {
          status: cancelled ? 'cancelled' : 'error',
          wsName,
          ...(this.compiler?.getRuntimeDiagnostics
            ? { runtimeDiagnostics: this.compiler.getRuntimeDiagnostics() }
            : {}),
          ...(cancelled ? {} : { error: message }),
        });
      }
      throw error;
    } finally {
      signal?.removeEventListener('abort', relayAbort);
      if (this.running === controller) {
        this.running = undefined;
      }
    }
  }

  cancel(): void {
    const running = this.running;
    if (!running) return;
    running.abort(new DOMException('Compilation cancelled', 'AbortError'));
    this.compiler?.cancel();
    this.running = undefined;
    const next = { ...this.store.get(this.$state), status: 'cancelled' as const };
    delete next.error;
    this.store.set(this.$state, next);
  }
}
