import type {
  CompileArtifact,
  LatexCompileFile,
  LatexCompiler,
  LatexLiveDraftCompiler,
  LatexLiveDraftProbeRequest,
  LatexLiveDraftProbeResult,
  LatexLiveDraftRecord,
  LatexLiveDraftRenderRequest,
  LatexLiveDraftRenderResult,
  LatexLiveInstantRenderRequest,
  LatexLiveInstantRenderResult,
} from '@learnitive/latex-compiler';
import { projectFilePath } from '@learnitive/project-storage';
import { BusyTexBrowserCompiler } from './busytex-adapter';

export const LEARNITIVE_LIVE_DRAFT_DIR = '__learnitive_live__';
export const LEARNITIVE_LIVE_DRAFT_WRAPPER_NAME = 'probe.tex';
export const LEARNITIVE_LIVE_DRAFT_LUA_NAME = 'probe.lua';
export const LEARNITIVE_LIVE_DRAFT_RECORD_NAME = 'layout.jsonl';

const encoder = new TextEncoder();
const INSTANT_WARM_ROOT = projectFilePath('__learnitive_live_instant_warm__.tex');

function asTextFile(path: ReturnType<typeof projectFilePath>, text: string): LatexCompileFile {
  // TypeScript 6 models TextEncoder output as Uint8Array<ArrayBufferLike>,
  // while the compiler file contract intentionally requires transferable
  // ArrayBuffer-backed bytes. Copy into a concrete ArrayBuffer-backed view.
  const encoded = encoder.encode(text);
  const content = new Uint8Array(encoded.byteLength);
  content.set(encoded);
  return { path, content };
}

function validateRootFile(rootFile: string): void {
  if (!rootFile || /[\r\n{}\\]/u.test(rootFile)) {
    throw new Error('Live PDF requires a canonical project root path without TeX-special path delimiters.');
  }
}

function probePath(name: string): ReturnType<typeof projectFilePath> {
  return projectFilePath(`${LEARNITIVE_LIVE_DRAFT_DIR}/${name}`);
}

function instantWrapperPath(rootFile: string): ReturnType<typeof projectFilePath> {
  const normalized = rootFile.replace(/\\/gu, '/');
  const slash = normalized.lastIndexOf('/');
  const dir = slash >= 0 ? normalized.slice(0, slash + 1) : '';
  return projectFilePath(`${dir}__learnitive_live_instant__.tex`);
}

function rootPreamble(files: readonly LatexCompileFile[], rootFile: string): string | undefined {
  const normalized = rootFile.replace(/\\/gu, '/').replace(/^\.\/+/, '');
  const file = files.find((candidate) => String(candidate.path).replace(/\\/gu, '/').replace(/^\.\/+/, '') === normalized);
  if (!file) return undefined;
  const source = new TextDecoder('utf-8').decode(file.content);
  const begin = /\\begin\s*\{document\}/u.exec(source);
  if (!begin || begin.index === undefined) return undefined;
  return source.slice(0, begin.index);
}

function instantDocumentSource(preamble: string, text: string, hsize: number): string {
  const width = Math.min(520, Math.max(72, Number.isFinite(hsize) ? hsize : 345));
  // The block is compiled in a disposable in-memory document. A minipage pins
  // line wrapping to the exact R5 SyncTeX width while PDF.js later crops away
  // margins/page furniture. Nothing here is persisted to the user's project.
  return `${preamble}\n\\begin{document}\n\\pagestyle{empty}\n\\thispagestyle{empty}\n\\begin{minipage}{${width.toFixed(3)}pt}\n${text}\n\\end{minipage}\n\\end{document}\n`;
}

/**
 * R4 is deliberately a tiny Learnitive-owned runtime certificate. It does not
 * include or instrument the user's manuscript. The only things it proves are
 * the capabilities R5 actually needs before it may trust this BusyTeX runtime:
 * LuaLaTeX execution, directlua, a Lua-written MEMFS record at shipout, and a
 * genuine PDF payload.
 */
function probeDocumentSource(): string {
  return String.raw`\documentclass{article}
\directlua{dofile("${LEARNITIVE_LIVE_DRAFT_LUA_NAME}")}
\AddToHook{shipout/before}{\directlua{learnitive_live_shipout()}}
\AddToHook{enddocument/afterlastpage}{\directlua{learnitive_live_finalize()}}
\begin{document}
Learnitive Live PDF runtime feasibility.\par
\rule{2cm}{1pt}
\newpage
Second feasibility page.
\end{document}
`;
}

function liveLuaProbe(): string {
  return String.raw`local out = assert(io.open("${LEARNITIVE_LIVE_DRAFT_RECORD_NAME}", "w"))
local page_no = 0

local function emit(line)
  out:write(line, "\n")
  out:flush()
end

emit('{"type":"runtime","name":"directlua"}')

function learnitive_live_shipout()
  page_no = page_no + 1
  emit(string.format('{"type":"page","page":%d}', page_no))
end

function learnitive_live_finalize()
  if out then
    out:flush()
    out:close()
    out = nil
  end
end
`;
}

function findRecordArtifact(
  artifacts: readonly CompileArtifact[] | undefined,
): CompileArtifact | undefined {
  const expected = `${LEARNITIVE_LIVE_DRAFT_DIR}/${LEARNITIVE_LIVE_DRAFT_RECORD_NAME}`;
  return artifacts?.find((artifact) => {
    const path = artifact.path.replace(/^\/+/, '');
    return path === expected || path === LEARNITIVE_LIVE_DRAFT_RECORD_NAME;
  });
}

function recordLines(text: string): string[] {
  const physicalLines = text.split(/\r?\n/u);
  if (physicalLines.length > 1 || !text.includes('\\n')) return physicalLines;

  // R31 Build Fix 1 accidentally emitted the two literal characters "\\n"
  // between NDJSON records because the Lua source lived inside String.raw. Keep
  // this narrow fallback so an already-written probe artifact remains readable
  // while all new probes emit real newline bytes.
  return text.split('\\n');
}

function parseRecords(text: string): LatexLiveDraftRecord[] {
  const records: LatexLiveDraftRecord[] = [];
  for (const line of recordLines(text)) {
    const trimmed = line.trim();
    if (!trimmed) continue;
    let raw: Record<string, unknown>;
    try {
      const parsed = JSON.parse(trimmed) as unknown;
      if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) continue;
      raw = parsed as Record<string, unknown>;
    } catch {
      continue;
    }
    const type = typeof raw.type === 'string' ? raw.type : 'unknown';
    const page = typeof raw.page === 'number' ? raw.page : undefined;
    records.push({
      type,
      ...(page === undefined ? {} : { page }),
      ...(typeof raw.name === 'string' ? { name: raw.name } : {}),
      raw,
    });
  }
  return records;
}


function certificateReason(input: {
  compileSucceeded: boolean;
  artifactFound: boolean;
  recordBytes: number;
  directLua: boolean;
  pageCount: number;
  pdfBytes: number;
  logTail: string;
}): string {
  if (!input.compileSucceeded) {
    return `LuaLaTeX failed.${input.logTail ? `\n${input.logTail}` : ''}`;
  }
  if (!input.artifactFound) {
    return 'LuaLaTeX succeeded but the MEMFS runtime record could not be read.';
  }
  if (input.recordBytes === 0) {
    return 'LuaLaTeX succeeded and the MEMFS runtime record was found, but it was empty.';
  }
  if (!input.directLua) {
    return 'MEMFS record readback succeeded, but the directlua runtime marker was missing.';
  }
  if (input.pageCount === 0) {
    return input.pdfBytes > 0
      ? 'PDF bytes were returned, but no Lua/MEMFS shipout record was visible.'
      : 'directlua and MEMFS readback passed, but no shipout record was visible.';
  }
  if (input.pdfBytes === 0) {
    return 'Lua/MEMFS shipout evidence passed, but BusyTeX returned no PDF bytes.';
  }
  return 'Live PDF runtime certificate failed.';
}

function usefulLogTail(log: string): string {
  const lines = log
    .split(/\r?\n/u)
    .map((line) => line.trimEnd())
    .filter(Boolean);
  const isRuntimeNoise = (line: string) =>
    /keepRuntimeAlive|async operation|emscripten_force_exit|halting execution but not exiting|^\d+ hlist,|attribute_list|glue_spec/u.test(
      line,
    );
  const diagnostic = /^(?:!|LaTeX Error:|Package .* Error:|pdfTeX error:|LuaTeX error:)|Fatal error occurred|Emergency stop|cannot open .* for reading|not found/u;
  const diagnosticIndex = lines.findIndex((line) => diagnostic.test(line));
  if (diagnosticIndex >= 0) {
    return lines
      .slice(diagnosticIndex, diagnosticIndex + 5)
      .filter((line) => !isRuntimeNoise(line))
      .join('\n')
      .slice(0, 1_200);
  }
  return lines
    .filter((line) => !isRuntimeNoise(line))
    .slice(-5)
    .join('\n')
    .slice(0, 1_200);
}

/**
 * Dedicated BusyTeX Lua compiler for R4/R5. It never shares
 * ProjectCompileService state. R4 probe() is self-contained; R5 render()
 * compiles the canonical project snapshot directly and never wraps the user's
 * root document in feasibility instrumentation.
 */
export class BusyTexLiveLuaCompiler implements LatexLiveDraftCompiler {
  private compiler: LatexCompiler | undefined;
  /** R6 owns a second worker so superseding exact R5 compiles never destroys the warm instant lane. */
  private instantCompiler: LatexCompiler | undefined;
  private instantWarmup: Promise<void> | undefined;

  constructor(
    private readonly createCompiler: () => LatexCompiler = () => new BusyTexBrowserCompiler(),
  ) {}

  private getCompiler(): LatexCompiler {
    const compiler = this.compiler ?? this.createCompiler();
    this.compiler = compiler;
    return compiler;
  }

  private getInstantCompiler(): LatexCompiler {
    const compiler = this.instantCompiler ?? this.createCompiler();
    this.instantCompiler = compiler;
    return compiler;
  }

  async probe(request: LatexLiveDraftProbeRequest): Promise<LatexLiveDraftProbeResult> {
    request.signal?.throwIfAborted();
    const compiler = this.getCompiler();
    if (!compiler.supports('lualatex')) {
      return {
        ok: false,
        engine: 'lualatex',
        runtime: 'busytex',
        directLua: false,
        memfsRecords: false,
        pageCount: 0,
        records: [],
        fonts: [],
        hasGlyphs: false,
        hasRules: false,
        hasWhatsits: false,
        hasMultiplePages: false,
        log: '',
        durationMs: 0,
        certificate: {
          compileSucceeded: false,
          recordArtifactFound: false,
          recordBytes: 0,
          directLuaRecordFound: false,
          pageRecords: 0,
          pdfBytes: 0,
          artifactReadbackAttempts: [],
        },
        reason: 'The configured browser compiler does not expose LuaLaTeX.',
      };
    }

    const rootFile = probePath(LEARNITIVE_LIVE_DRAFT_WRAPPER_NAME);
    const files: LatexCompileFile[] = [
      asTextFile(probePath(LEARNITIVE_LIVE_DRAFT_LUA_NAME), liveLuaProbe()),
      asTextFile(rootFile, probeDocumentSource()),
    ];
    const started = performance.now();
    const result = await compiler.compile({
      compiler: 'lualatex',
      mode: 'full',
      auxiliaryPasses: 'none',
      rootFile,
      files,
      signal: request.signal,
      onProgress: request.onProgress,
    });
    request.signal?.throwIfAborted();

    const artifact = findRecordArtifact(result.artifacts);
    const records = artifact?.textContent ? parseRecords(artifact.textContent) : [];
    const directLua = records.some(
      (record) => record.type === 'runtime' && record.name === 'directlua',
    );
    const pageCount = records.filter((record) => record.type === 'page').length;
    const recordBytes = artifact?.data?.byteLength ?? artifact?.size ?? (artifact?.textContent ? encoder.encode(artifact.textContent).byteLength : 0);
    const memfsRecords = Boolean(artifact && recordBytes > 0 && records.length > 0);
    const pdfBytes = result.pdf?.byteLength ?? 0;
    const hasPdf = pdfBytes > 0;
    const ok = Boolean(result.success && directLua && memfsRecords && pageCount > 0 && hasPdf);
    const tail = usefulLogTail(result.log);
    const certificate = {
      compileSucceeded: result.success,
      recordArtifactFound: Boolean(artifact),
      ...(artifact ? { recordArtifactPath: artifact.path } : {}),
      recordBytes,
      directLuaRecordFound: directLua,
      pageRecords: pageCount,
      pdfBytes,
      artifactReadbackAttempts: result.runtimeDiagnostics?.artifactReadback?.attempts ?? [],
    };

    return {
      ok,
      engine: 'lualatex',
      runtime: 'busytex',
      directLua,
      memfsRecords,
      pageCount,
      records,
      fonts: [],
      hasGlyphs: false,
      hasRules: false,
      hasWhatsits: false,
      hasMultiplePages: pageCount > 1,
      log: result.log,
      durationMs: Math.max(result.durationMs, performance.now() - started),
      certificate,
      ...(result.pdf?.byteLength ? { pdf: result.pdf } : {}),
      ...(ok
        ? {}
        : {
            reason: certificateReason({
              compileSucceeded: result.success,
              artifactFound: Boolean(artifact),
              recordBytes,
              directLua,
              pageCount,
              pdfBytes,
              logTail: tail,
            }),
          }),
    };
  }

  async render(request: LatexLiveDraftRenderRequest): Promise<LatexLiveDraftRenderResult> {
    request.signal?.throwIfAborted();
    const rootFile = String(request.rootFile);
    validateRootFile(rootFile);
    const compiler = this.getCompiler();
    if (!compiler.supports('lualatex')) {
      return {
        ok: false,
        engine: 'lualatex',
        runtime: 'busytex',
        log: '',
        durationMs: 0,
        reason: 'Live PDF compile failed because the configured browser compiler does not expose LuaLaTeX.',
      };
    }

    const started = performance.now();
    const result = await compiler.compile({
      compiler: 'lualatex',
      mode: 'full',
      // Live PDF is an interactive preview path. One canonical LuaLaTeX pass
      // emits the PDF + SyncTeX pair immediately; manual Compile remains the
      // full bibliography/index/rerun path when the user wants a settled build.
      auxiliaryPasses: 'none',
      artifactReadback: 'none',
      rootFile: request.rootFile,
      files: request.files,
      signal: request.signal,
      onProgress: request.onProgress,
    });
    request.signal?.throwIfAborted();
    const ok = Boolean(result.success && result.pdf?.byteLength);
    const tail = usefulLogTail(result.log);
    return {
      ok,
      engine: 'lualatex',
      runtime: 'busytex',
      log: result.log,
      durationMs: Math.max(result.durationMs, performance.now() - started),
      ...(result.pdf?.byteLength ? { pdf: result.pdf } : {}),
      ...(result.synctex?.byteLength ? { synctex: result.synctex } : {}),
      ...(ok
        ? {}
        : {
            reason: result.success
              ? 'Live PDF compile completed but BusyTeX did not return PDF bytes.'
              : `Live PDF compile failed.${tail ? `\n${tail}` : ''}`,
          }),
    };
  }

  async warmInstant(): Promise<void> {
    if (this.instantWarmup) return this.instantWarmup;
    const compiler = this.getInstantCompiler();
    if (!compiler.supports('lualatex')) return;
    // Warm a dedicated LuaLaTeX worker after R5 succeeds so the first eligible
    // keystroke does not pay WASM/runtime startup. This document is Learnitive-owned
    // and never reaches project storage or the authoritative PDF state.
    this.instantWarmup = compiler.compile({
      compiler: 'lualatex',
      mode: 'fast',
      auxiliaryPasses: 'none',
      artifactReadback: 'none',
      rootFile: INSTANT_WARM_ROOT,
      files: [
        asTextFile(
          INSTANT_WARM_ROOT,
          String.raw`\documentclass{article}
\begin{document}
Learnitive instant preview warm-up.
\end{document}
`,
        ),
      ],
    }).then((result) => {
      if (!result.success || !result.pdf?.byteLength) {
        throw new Error('Instant Live PDF warm-up did not return a valid PDF.');
      }
    });
    await this.instantWarmup;
  }

  async renderInstant(request: LatexLiveInstantRenderRequest): Promise<LatexLiveInstantRenderResult> {
    request.signal?.throwIfAborted();
    if (this.instantWarmup) await this.instantWarmup;
    request.signal?.throwIfAborted();
    const rootFile = String(request.rootFile);
    validateRootFile(rootFile);
    const preamble = rootPreamble(request.files, rootFile);
    if (preamble === undefined) {
      return {
        ok: false,
        engine: 'lualatex',
        runtime: 'busytex',
        log: '',
        durationMs: 0,
        reason: 'Instant Live PDF could not isolate the canonical root preamble.',
      };
    }
    const compiler = this.getInstantCompiler();
    if (!compiler.supports('lualatex')) {
      return {
        ok: false,
        engine: 'lualatex',
        runtime: 'busytex',
        log: '',
        durationMs: 0,
        reason: 'Instant Live PDF requires browser LuaLaTeX.',
      };
    }
    const wrapper = instantWrapperPath(rootFile);
    const files: LatexCompileFile[] = request.files
      .filter((file) => String(file.path) !== String(wrapper))
      .map((file) => {
        const content = new Uint8Array(file.content.byteLength);
        content.set(file.content);
        return { path: file.path, content };
      });
    files.push(asTextFile(wrapper, instantDocumentSource(preamble, request.text, request.hsize)));

    const started = performance.now();
    try {
      const result = await compiler.compile({
        compiler: 'lualatex',
        mode: 'fast',
        auxiliaryPasses: 'none',
        artifactReadback: 'none',
        rootFile: wrapper,
        files,
        signal: request.signal,
        onProgress: request.onProgress,
      });
      request.signal?.throwIfAborted();
      const ok = Boolean(result.success && result.pdf?.byteLength);
      const tail = usefulLogTail(result.log);
      return {
        ok,
        engine: 'lualatex',
        runtime: 'busytex',
        log: result.log,
        durationMs: Math.max(result.durationMs, performance.now() - started),
        ...(result.pdf?.byteLength ? { pdf: result.pdf } : {}),
        ...(ok ? {} : { reason: result.success
          ? 'Instant Live PDF compile returned no PDF bytes.'
          : `Instant Live PDF block compile failed.${tail ? `\n${tail}` : ''}` }),
      };
    } catch (error) {
      request.signal?.throwIfAborted();
      return {
        ok: false,
        engine: 'lualatex',
        runtime: 'busytex',
        log: '',
        durationMs: Math.max(0, performance.now() - started),
        reason: error instanceof Error ? error.message : 'Instant Live PDF block compile failed.',
      };
    }
  }

  cancelInstant(): void {
    this.instantCompiler?.cancel();
    this.instantWarmup = undefined;
  }

  disposeInstant(): void {
    this.instantCompiler?.dispose();
    this.instantCompiler = undefined;
    this.instantWarmup = undefined;
  }

  cancel(): void {
    this.compiler?.cancel();
  }

  dispose(): void {
    this.compiler?.dispose();
    this.instantCompiler?.dispose();
    this.compiler = undefined;
    this.instantCompiler = undefined;
    this.instantWarmup = undefined;
  }
}
