import {
  buildLatexCompileResult,
  planBibliographyPasses,
  unsupportedFallbackMakeindexDiagnostic,
  type LatexCompileRequest,
  type LatexCompileResult,
  type LatexCompiler,
  type LatexCompilerPackageResolution,
  type LatexCompilerRuntimeDiagnostics,
} from '@learnitive/latex-compiler';
import type { LearnitiveCompiler } from '@learnitive/project-storage';
import { abortable } from './abortable';
import { ensureTexliveServiceWorker } from './texlive-service-worker';

const ENGINE_SCRIPT = '/compiler/swiftlatex-r3-v1/PdfTeXEngine.js';
const TEXLIVE_ENDPOINT = '/texlive/swiftlatex-r3-v1/';
const TEXT_MANIFEST = '/texlive/cache-manifest-text.txt';
const BINARY_MANIFEST = '/texlive/cache-manifest-binary.txt';
const BUNDLED_CACHE = '/texlive/cache/';
const TEXT_EXTENSIONS = /\.(?:tex|ltx|sty|cls|bst|bib|cfg|def|fd|clo|map|enc|txt|csv|dat)$/i;
type SwiftCompileResult = {
  status: number;
  log: string;
  pdf?: Uint8Array | ArrayBuffer;
  synctex?: Uint8Array | ArrayBuffer;
};

type SwiftEngine = {
  loadEngine(): Promise<void>;
  isReady(): boolean;
  setTexliveEndpoint(endpoint: string): void;
  writeMemFSFile(path: string, content: string): void;
  writeBinaryMemFSFile(path: string, content: ArrayBuffer): void;
  makeMemFSFolder(path: string): void;
  setEngineMainFile(path: string): void;
  compileLaTeX(): Promise<SwiftCompileResult>;
  flushCache(): void;
  closeWorker?(): void;
};

declare global {
  interface Window {
    PdfTeXEngine?: new () => SwiftEngine;
  }
}

function loadScriptOnce(src: string): Promise<void> {
  const existing = document.querySelector<HTMLScriptElement>(`script[data-interactive-compiler="${src}"]`);
  if (existing?.dataset.loaded === 'true') {
    return Promise.resolve();
  }

  return new Promise((resolve, reject) => {
    const script = existing ?? document.createElement('script');
    script.src = src;
    script.dataset.learnitiveCompiler = src;
    script.addEventListener(
      'load',
      () => {
        script.dataset.loaded = 'true';
        resolve();
      },
      { once: true },
    );
    script.addEventListener(
      'error',
      () => reject(new Error(`Failed to load browser TeX engine asset: ${src}`)),
      { once: true },
    );
    if (!existing) {
      document.head.appendChild(script);
    }
  });
}

function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
  return bytes.buffer.slice(
    bytes.byteOffset,
    bytes.byteOffset + bytes.byteLength,
  ) as ArrayBuffer;
}

function ensureDirectories(engine: SwiftEngine, paths: readonly string[]): void {
  const directories = new Set<string>();
  for (const path of paths) {
    const segments = path.split('/');
    for (let index = 1; index < segments.length; index += 1) {
      const directory = segments.slice(0, index).join('/');
      if (directory) directories.add(directory);
    }
  }
  for (const directory of [...directories].sort((a, b) => a.length - b.length)) {
    try {
      engine.makeMemFSFolder(directory);
    } catch {
      // Existing MEMFS directories are harmless.
    }
  }
}

function looksLikeHtml(text: string): boolean {
  const head = text.slice(0, 120).toLowerCase();
  return head.includes('<!doctype') || head.includes('<html');
}

async function loadManifest(path: string, signal?: AbortSignal): Promise<string[]> {
  const response = await fetch(path, { cache: 'force-cache', signal });
  if (!response.ok) {
    throw new Error(`Bundled TeX Live manifest is unavailable: ${path}`);
  }
  return (await response.text())
    .split(/\r?\n/u)
    .map((line) => line.trim())
    .filter(Boolean);
}

async function fetchTextFile(name: string, signal?: AbortSignal): Promise<string | undefined> {
  const response = await fetch(`${BUNDLED_CACHE}${encodeURIComponent(name)}`, {
    cache: 'force-cache',
    signal,
  });
  if (!response.ok) return undefined;
  const text = await response.text();
  return looksLikeHtml(text) ? undefined : text;
}

async function fetchBinaryFile(
  name: string,
  signal?: AbortSignal,
): Promise<ArrayBuffer | undefined> {
  const response = await fetch(`${BUNDLED_CACHE}${encodeURIComponent(name)}`, {
    cache: 'force-cache',
    signal,
  });
  if (!response.ok) return undefined;
  const buffer = await response.arrayBuffer();
  const head = new TextDecoder().decode(
    new Uint8Array(buffer, 0, Math.min(120, buffer.byteLength)),
  );
  return buffer.byteLength > 0 && !looksLikeHtml(head) ? buffer : undefined;
}

export class SwiftLatexBrowserCompiler implements LatexCompiler {
  readonly backend = 'swiftlatex' as const;
  private engine: SwiftEngine | undefined;
  private loading: Promise<void> | undefined;
  private onDemandAvailable = false;
  private fallbackLoaded = false;
  private resolutionListenerInstalled = false;
  private activePackageTelemetry:
    | { startedAt?: number; lastAt?: number; durationMs: number; packagesFetched: number }
    | undefined;
  private runtime: LatexCompilerRuntimeDiagnostics = {
    status: 'idle',
    backend: 'swiftlatex',
    serviceWorker: 'unknown',
    packageResolutionChain: [],
  };

  supports(compiler: LearnitiveCompiler): boolean {
    return compiler === 'pdflatex';
  }

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

  private installResolutionListener(): void {
    if (this.resolutionListenerInstalled || !('serviceWorker' in navigator)) return;
    navigator.serviceWorker.addEventListener('message', this.onServiceWorkerMessage);
    this.resolutionListenerInstalled = true;
  }

  private readonly onServiceWorkerMessage = (event: MessageEvent): void => {
    const data = event.data;
    if (!data || data.type !== 'interactive-texlive-resolution') return;
    const sources = new Set([
      'cache',
      'bundled',
      'static-mirror',
      'remote',
      'format',
      'miss',
    ]);
    if (typeof data.requested !== 'string' || !sources.has(data.source)) return;
    if (
      this.activePackageTelemetry &&
      (data.source === 'bundled' || data.source === 'static-mirror' || data.source === 'remote')
    ) {
      const now = performance.now();
      this.activePackageTelemetry.startedAt ??= now;
      this.activePackageTelemetry.lastAt = now;
      if (typeof data.durationMs === 'number' && Number.isFinite(data.durationMs)) {
        this.activePackageTelemetry.durationMs += Math.max(0, data.durationMs);
      }
      this.activePackageTelemetry.packagesFetched += 1;
    }
    this.runtime = {
      ...this.runtime,
      lastPackage: {
        requested: data.requested,
        ...(typeof data.resolved === 'string' ? { resolved: data.resolved } : {}),
        source: data.source,
      } as LatexCompilerPackageResolution,
    };
  };

  private async preloadFallback(
    engine: SwiftEngine,
    signal?: AbortSignal,
  ): Promise<number> {
    if (this.fallbackLoaded) return 0;
    signal?.throwIfAborted();
    const [textNames, binaryNames] = await Promise.all([
      loadManifest(TEXT_MANIFEST, signal),
      loadManifest(BINARY_MANIFEST, signal),
    ]);
    if (textNames.length + binaryNames.length === 0) {
      throw new Error('Bundled TeX Live fallback is empty');
    }

    let loadedFiles = 0;
    const tasks: Array<() => Promise<void>> = [
      ...textNames.map((name) => async () => {
        signal?.throwIfAborted();
        const content = await fetchTextFile(name, signal);
        if (content != null) {
          engine.writeMemFSFile(`/tex/${name}`, content);
          loadedFiles += 1;
        }
      }),
      ...binaryNames.map((name) => async () => {
        signal?.throwIfAborted();
        const content = await fetchBinaryFile(name, signal);
        if (content) {
          engine.writeBinaryMemFSFile(`/tex/${name}`, content);
          loadedFiles += 1;
        }
      }),
    ];

    for (let index = 0; index < tasks.length; index += 50) {
      signal?.throwIfAborted();
      const results = await Promise.allSettled(
        tasks.slice(index, index + 50).map((task) => task()),
      );
      const aborted = results.find(
        (result) =>
          result.status === 'rejected' &&
          result.reason instanceof DOMException &&
          result.reason.name === 'AbortError',
      );
      if (aborted?.status === 'rejected') throw aborted.reason;
    }
    this.fallbackLoaded = true;
    return loadedFiles;
  }


  async initialize(signal?: AbortSignal): Promise<void> {
    signal?.throwIfAborted();
    if (this.engine?.isReady() && this.runtime.status === 'ready') return;
    if (this.loading) return this.loading;

    this.loading = (async () => {
      this.runtime = {
        status: 'initializing',
        backend: 'swiftlatex',
        serviceWorker: 'unknown',
        packageResolutionChain: [],
      };
      try {
        this.installResolutionListener();
        const serviceWorkerSupported = 'serviceWorker' in navigator;
        this.onDemandAvailable = await ensureTexliveServiceWorker();
        this.runtime = {
          ...this.runtime,
          serviceWorker: this.onDemandAvailable
            ? 'controlled'
            : serviceWorkerSupported
              ? 'fallback-preload'
              : 'unsupported',
          packageResolutionChain: this.onDemandAvailable
            ? [
                'persistent browser cache',
                'bundled TeX Live core',
                'ls-R static TeX Live mirror',
                'SwiftLaTeX package server',
              ]
            : ['bundled TeX Live MEMFS preload'],
        };

        await loadScriptOnce(ENGINE_SCRIPT);
        signal?.throwIfAborted();
        const Engine = window.PdfTeXEngine;
        if (!Engine) {
          throw new Error('SwiftLaTeX pdfTeX runtime is unavailable');
        }
        const engine = new Engine();
        await abortable(engine.loadEngine(), signal, () => engine.closeWorker?.());
        signal?.throwIfAborted();
        engine.setTexliveEndpoint(TEXLIVE_ENDPOINT);
        this.engine = engine;

        // R14.24.1: initialization only loads the runtime. Package preload and
        // compilation are performed by the user's actual compile request, so
        // opening compiler settings/capability UI never triggers hidden TeX work.
        this.runtime = { ...this.runtime, status: 'ready' };
      } catch (error) {
        signal?.throwIfAborted();
        const message = error instanceof Error ? error.message : String(error);
        this.runtime = { ...this.runtime, status: 'error', error: message };
        this.engine?.closeWorker?.();
        this.engine = undefined;
        throw error;
      }
    })();

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

  async compile(request: LatexCompileRequest): Promise<LatexCompileResult> {
    if (!this.supports(request.compiler)) {
      throw new Error(`SwiftLaTeX fallback does not support ${request.compiler}`);
    }
    const totalStarted = performance.now();
    const runtimeWasWarm = Boolean(
      this.engine?.isReady() && this.runtime.status === 'ready',
    );
    this.activePackageTelemetry = { durationMs: 0, packagesFetched: 0 };

    try {
      request.onProgress?.({
        phase: 'initializing',
        message: 'Preparing browser pdfLaTeX runtime',
      });
      const initStarted = performance.now();
      await this.initialize(request.signal);
      const runtimeInitMs = runtimeWasWarm
        ? 0
        : Math.max(0, performance.now() - initStarted);
      const engine = this.engine;
      if (!engine) throw new Error('SwiftLaTeX failed to initialize');

      const plan = planBibliographyPasses(request.files);
      let measuredPackageFetchMs = 0;
      let engineCompileMs = 0;
      let bibliographyMs = 0;
      request.onProgress?.({ phase: 'mounting-files', message: 'Mounting project files' });
      engine.flushCache();
      if (!this.onDemandAvailable) {
        // An uncontrolled page cannot service worker package requests. Re-seed
        // the bundled subset for the real compile after clearing the work directory.
        this.fallbackLoaded = false;
        request.onProgress?.({
          phase: 'downloading-packages',
          message: 'Preloading bundled TeX Live packages into browser memory',
        });
        const packageStarted = performance.now();
        const loadedFiles = await this.preloadFallback(engine, request.signal);
        measuredPackageFetchMs += Math.max(0, performance.now() - packageStarted);
        if (this.activePackageTelemetry) {
          this.activePackageTelemetry.packagesFetched += loadedFiles;
        }
      }
      ensureDirectories(engine, request.files.map((file) => file.path));

      const decoder = new TextDecoder();
      for (const file of request.files) {
        request.signal?.throwIfAborted();
        if (TEXT_EXTENSIONS.test(file.path)) {
          engine.writeMemFSFile(file.path, decoder.decode(file.content));
        } else {
          engine.writeBinaryMemFSFile(file.path, toArrayBuffer(file.content));
        }
      }

      const escapedRoot = request.rootFile.replaceAll('\\', '/');
      // Compile the real project root directly. SwiftLaTeX's pdfTeX fallback
      // does not expose native SyncTeX, so no wrapper/test document is injected.
      engine.setEngineMainFile(escapedRoot);
      request.onProgress?.({ phase: 'compiling', message: 'Running real browser pdfLaTeX' });
      const passes: SwiftCompileResult[] = [];
      const compilePass = () =>
        abortable(engine.compileLaTeX(), request.signal, () => {
          engine.closeWorker?.();
          if (this.engine === engine) this.engine = undefined;
        });

      let passStarted = performance.now();
      let result = await compilePass();
      engineCompileMs += Math.max(0, performance.now() - passStarted);
      passes.push(result);

      const desiredPasses = plan.bibtex ? 3 : plan.rerun ? 2 : 1;
      for (let pass = 1; pass < desiredPasses && result.status === 0; pass += 1) {
        request.signal?.throwIfAborted();
        const bibliographyPass = plan.bibtex && pass === 1;
        request.onProgress?.({
          phase: bibliographyPass ? 'bibliography' : 'rerun',
          message: bibliographyPass ? 'Resolving bibliography' : 'Resolving references',
        });
        passStarted = performance.now();
        result = await compilePass();
        const elapsed = Math.max(0, performance.now() - passStarted);
        if (bibliographyPass) bibliographyMs += elapsed;
        else engineCompileMs += elapsed;
        passes.push(result);
      }

      const pdfReadyStarted = performance.now();
      const log = passes
        .map((pass, index) => `--- TeX pass ${index + 1} ---\n${pass.log}`)
        .join('\n\n');
      const pdf = result.pdf
        ? result.pdf instanceof Uint8Array
          ? result.pdf
          : new Uint8Array(result.pdf)
        : undefined;
      const synctex = result.synctex
        ? result.synctex instanceof Uint8Array
          ? result.synctex
          : new Uint8Array(result.synctex)
        : undefined;
      const exitCode = result.status;
      request.onProgress?.({ phase: 'finalizing', percent: 100 });

      const packageTelemetry = this.activePackageTelemetry;
      const observedPackageFetchMs =
        packageTelemetry && packageTelemetry.durationMs > 0
          ? packageTelemetry.durationMs
          : packageTelemetry?.startedAt !== undefined && packageTelemetry.lastAt !== undefined
            ? Math.max(0, packageTelemetry.lastAt - packageTelemetry.startedAt)
            : 0;
      const packageFetchMs = measuredPackageFetchMs + observedPackageFetchMs;
      const pdfReadyMs = Math.max(0, performance.now() - pdfReadyStarted);
      const totalMs = Math.max(0, performance.now() - totalStarted);

      const compileResult = buildLatexCompileResult({
        backend: this.backend,
        compiler: request.compiler,
        rootFile: request.rootFile,
        compileMode: request.mode ?? 'fast',
        exitCode,
        ...(pdf ? { pdf } : {}),
        ...(synctex ? { synctex } : {}),
        log,
        durationMs: totalMs,
        bibliography: plan,
        performance: {
          runtimeInitMs,
          packageFetchMs,
          bibliographyMs,
          engineCompileMs,
          pdfReadyMs,
          totalMs,
          runtimeWasWarm,
          packagesFetched: packageTelemetry?.packagesFetched ?? 0,
        },
      });
      const withRuntime = {
        ...compileResult,
        runtimeDiagnostics: this.getRuntimeDiagnostics(),
      };
      if (!plan.makeindex) return withRuntime;
      return {
        ...withRuntime,
        success: false,
        diagnostics: [
          unsupportedFallbackMakeindexDiagnostic(),
          ...withRuntime.diagnostics,
        ],
      };
    } finally {
      this.activePackageTelemetry = undefined;
    }
  }

  cancel(): void {
    this.engine?.closeWorker?.();
    this.engine = undefined;
    this.fallbackLoaded = false;
    if (this.runtime.status !== 'error') {
      this.runtime = { ...this.runtime, status: 'idle' };
    }
  }

  dispose(): void {
    this.cancel();
    if (this.resolutionListenerInstalled && 'serviceWorker' in navigator) {
      navigator.serviceWorker.removeEventListener('message', this.onServiceWorkerMessage);
      this.resolutionListenerInstalled = false;
    }
  }
}
