import { parseLatexDiagnostics } from './diagnostics';
import type {
  CompileArtifact,
  LatexBibliographyPlan,
  LatexCompileResult,
  LatexCompilerBackend,
  LatexCompileMode,
  LatexCompilePerformance,
} from './types';
import type { ProjectFilePath, LearnitiveCompiler } from '@learnitive/project-storage';

function outputStem(rootFile?: ProjectFilePath): string {
  const root = rootFile ? String(rootFile) : 'main.tex';
  return root.replace(/\.[^./]+$/u, '');
}

const FATAL_TEX_LOG_PATTERNS = [
  /program exited \(with status:\s*[1-9]\d*\)/iu,
  /(?:pdf|lua|xe)?tex fatal error/iu,
  /fatal error occurred/iu,
  /emergency stop\.?/iu,
  /cannot open (?:encoding|font|map) file for reading/iu,
] as const;

function hasReadablePdfHeader(pdf?: Uint8Array): boolean {
  return Boolean(
    pdf &&
      pdf.byteLength >= 4 &&
      pdf[0] === 0x25 &&
      pdf[1] === 0x50 &&
      pdf[2] === 0x44 &&
      pdf[3] === 0x46,
  );
}

function hasFatalTexLog(log: string): boolean {
  return FATAL_TEX_LOG_PATTERNS.some((pattern) => pattern.test(log));
}

function knownArtifacts(input: {
  rootFile?: ProjectFilePath;
  pdf?: Uint8Array;
  synctex?: Uint8Array;
  log: string;
}): CompileArtifact[] {
  const stem = outputStem(input.rootFile);
  const artifacts: CompileArtifact[] = [
    {
      path: `${stem}.log`,
      kind: 'log',
      mimeType: 'text/plain;charset=utf-8',
      size: new TextEncoder().encode(input.log).byteLength,
      generated: true,
      text: true,
      textContent: input.log,
    },
  ];
  if (input.pdf) {
    artifacts.unshift({
      path: `${stem}.pdf`,
      kind: 'pdf',
      mimeType: 'application/pdf',
      size: input.pdf.byteLength,
      generated: true,
      data: input.pdf,
    });
  }
  if (input.synctex) {
    artifacts.push({
      path: `${stem}.synctex`,
      kind: 'synctex',
      mimeType: 'application/octet-stream',
      size: input.synctex.byteLength,
      generated: true,
      data: input.synctex,
    });
  }
  return artifacts;
}

export function buildLatexCompileResult(input: {
  backend: LatexCompilerBackend;
  compiler: LearnitiveCompiler;
  compileMode?: LatexCompileMode;
  rootFile?: ProjectFilePath;
  exitCode: number;
  /** Native runtime success flag when the backend exposes one. */
  reportedSuccess?: boolean;
  pdf?: Uint8Array;
  synctex?: Uint8Array;
  log: string;
  durationMs: number;
  bibliography: LatexBibliographyPlan;
  artifacts?: readonly CompileArtifact[];
  performance?: LatexCompilePerformance;
}): LatexCompileResult {
  const diagnostics = parseLatexDiagnostics(input.log);
  const pdfReadable = hasReadablePdfHeader(input.pdf);
  const fatalLog =
    hasFatalTexLog(input.log) || diagnostics.some((item) => item.severity === 'error');
  const runtimeFailed = input.reportedSuccess === false;
  const authoritativeFailure =
    input.exitCode !== 0 || runtimeFailed || fatalLog || !pdfReadable;
  // Emscripten/BusyTeX may keep the module alive and surface a wrapper zero even
  // after the TeX subprocess failed. Never expose that wrapper zero as the
  // authoritative compile status when another trusted signal says the compile
  // failed or the produced PDF is unreadable/missing.
  const exitCode = authoritativeFailure
    ? input.exitCode === 0
      ? 1
      : input.exitCode
    : 0;
  const success = !authoritativeFailure;
  const merged = new Map<string, CompileArtifact>();
  for (const artifact of knownArtifacts(input)) merged.set(artifact.path, artifact);
  for (const artifact of input.artifacts ?? []) merged.set(artifact.path, artifact);
  return {
    success,
    backend: input.backend,
    compiler: input.compiler,
    compileMode: input.compileMode ?? 'full',
    exitCode,
    ...(input.pdf ? { pdf: input.pdf } : {}),
    ...(input.synctex ? { synctex: input.synctex } : {}),
    log: input.log,
    diagnostics,
    durationMs: input.durationMs,
    ...(input.performance ? { performance: input.performance } : {}),
    bibliography: input.bibliography,
    artifacts: [...merged.values()],
  };
}
