import { useEffect, useMemo, useRef, useState } from "react";
import { EngineSupervisor } from "../regex/execution/EngineSupervisor";
import {
  DEFAULT_REGEX_LIMITS,
  utf8ByteLength,
} from "../regex/execution/request-limits";
import type {
  RegexEngineOptions,
  RegexFlavourId,
} from "../regex/model/flavour";
import type { CaptureDefinition } from "../regex/model/syntax";
import {
  canExportAppliedCorpus,
  corpusAppliedDownloadName,
  createAppliedCorpusZip,
  serializeCorpusReport,
  type CorpusReportFormat,
} from "../regex/corpus/corpus-export";
import {
  corpusTotalBytes,
  createPastedCorpusDocument,
  readCorpusFiles,
} from "../regex/corpus/corpus-input";
import { runCorpusBatch } from "../regex/corpus/corpus-runner";
import type {
  CorpusBatchResult,
  CorpusDocument,
  CorpusDocumentResult,
  CorpusOperation,
  CorpusProgress,
  CorpusSubjectMode,
} from "../regex/corpus/corpus.types";

const CORPUS_FILE_ACCEPT =
  "text/*,.asc,.cfg,.conf,.csv,.htm,.html,.ini,.json,.jsonl,.log,.md,.ndjson,.properties,.rtf,.sql,.svg,.toml,.tsv,.txt,.xml,.yaml,.yml";

function downloadBlob(blob: Blob, name: string): void {
  const url = URL.createObjectURL(blob);
  const anchor = document.createElement("a");
  anchor.href = url;
  anchor.download = name;
  anchor.click();
  window.setTimeout(() => URL.revokeObjectURL(url), 1_000);
}

function resultCount(result: CorpusDocumentResult): string {
  if (result.status === "complete") return result.matchCount.toLocaleString();
  if (result.status === "partial") {
    return `≥${result.matchCount.toLocaleString()}`;
  }
  return "—";
}

function corpusMiB(bytes: number): string {
  return `${(bytes / 1024 / 1024).toFixed(bytes === 0 ? 0 : 2)} MiB`;
}

function CorpusResultTable({
  batch,
  allowContentExport,
  selectedDocumentId,
  onInspect,
}: {
  readonly batch: CorpusBatchResult;
  readonly allowContentExport: boolean;
  readonly selectedDocumentId?: string;
  readonly onInspect: (result: CorpusDocumentResult) => void;
}) {
  return (
    <div className="corpus-table-scroll">
      <table aria-label="Corpus document results">
        <thead>
          <tr>
            <th scope="col">Document</th>
            <th scope="col">Status</th>
            <th scope="col">Matches</th>
            <th scope="col">Captures</th>
            <th scope="col">Lines</th>
            <th scope="col">Input</th>
            <th scope="col">Output</th>
            <th scope="col">Change</th>
            <th scope="col">Engine time</th>
            <th scope="col">Actions</th>
          </tr>
        </thead>
        <tbody>
          {batch.documents.map((result) => {
            const downloadable =
              allowContentExport &&
              result.status === "complete" &&
              result.output !== undefined;
            return (
              <tr
                key={result.documentId}
                className={`corpus-${result.status}${
                  selectedDocumentId === result.documentId ? " is-selected" : ""
                }`}
              >
                <th scope="row">
                  <span className="corpus-document-name">{result.name}</span>
                  {result.message ? <small>{result.message}</small> : null}
                </th>
                <td>
                  <span className={`status corpus-status-${result.status}`}>
                    {result.status.replace("-", " ")}
                  </span>
                </td>
                <td>{resultCount(result)}</td>
                <td>
                  {Math.max(
                    0,
                    result.captureSummaries.filter(
                      (capture) => capture.groupNumber !== 0,
                    ).length,
                  ).toLocaleString()}
                  {result.captureSummariesTruncated ? "+" : ""}
                </td>
                <td>{result.lineCount.toLocaleString()}</td>
                <td>{result.inputBytes.toLocaleString()} B</td>
                <td>
                  {result.outputBytes === undefined
                    ? "—"
                    : `${result.outputBytes.toLocaleString()} B`}
                </td>
                <td>
                  {result.changed === true
                    ? "Changed"
                    : result.changed === false
                      ? "Unchanged"
                      : "—"}
                </td>
                <td>{result.elapsedMs.toFixed(2)} ms</td>
                <td>
                  <div className="corpus-row-actions">
                    <button
                      type="button"
                      className="secondary-button corpus-row-download"
                      aria-pressed={
                        selectedDocumentId === result.documentId
                          ? "true"
                          : "false"
                      }
                      onClick={() => onInspect(result)}
                    >
                      Inspect
                    </button>
                    {batch.operation === "replace" ? (
                      <button
                        type="button"
                        className="secondary-button corpus-row-download"
                        disabled={!downloadable}
                        title={
                          result.status !== "complete"
                            ? "Incomplete applied output cannot be exported."
                            : !allowContentExport
                              ? "Confirm the content-export warning first."
                              : undefined
                        }
                        onClick={() => {
                          if (!downloadable || result.output === undefined)
                            return;
                          downloadBlob(
                            new Blob([result.output], {
                              type: "text/plain;charset=utf-8",
                            }),
                            corpusAppliedDownloadName(result),
                          );
                        }}
                      >
                        Download
                      </button>
                    ) : null}
                  </div>
                </td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
}

export interface CorpusPanelProps {
  readonly active: boolean;
  readonly flavour: RegexFlavourId;
  readonly flavourVersion?: string;
  readonly options: RegexEngineOptions;
  readonly pattern: string;
  readonly flags: readonly string[];
  readonly captureMetadata: readonly CaptureDefinition[];
  readonly patternAccepted: boolean;
  readonly replacement: string;
  readonly replacementAccepted: boolean;
  readonly onReplacementChange: (value: string) => void;
  readonly timeoutMs: number;
}

export function CorpusPanel({
  active,
  flavour,
  flavourVersion,
  options,
  pattern,
  flags,
  captureMetadata,
  patternAccepted,
  replacement,
  replacementAccepted,
  onReplacementChange,
  timeoutMs,
}: CorpusPanelProps) {
  const [documents, setDocuments] = useState<readonly CorpusDocument[]>([]);
  const [operation, setOperation] = useState<CorpusOperation>("match");
  const [subjectMode, setSubjectMode] = useState<CorpusSubjectMode>("document");
  const [pastedName, setPastedName] = useState("pasted-text.txt");
  const [pastedText, setPastedText] = useState("");
  const [batch, setBatch] = useState<CorpusBatchResult>();
  const [progress, setProgress] = useState<CorpusProgress>();
  const [status, setStatus] = useState(
    "Corpus documents stay in this browser tab and are never uploaded.",
  );
  const [running, setRunning] = useState(false);
  const [importing, setImporting] = useState(false);
  const [allowContentExport, setAllowContentExport] = useState(false);
  const [reportFormat, setReportFormat] = useState<CorpusReportFormat>("json");
  const [selectedDocumentId, setSelectedDocumentId] = useState<string>();
  const [exportingZip, setExportingZip] = useState(false);
  const fileInput = useRef<HTMLInputElement>(null);
  const engine = useRef<EngineSupervisor | undefined>(undefined);
  const runController = useRef<AbortController | undefined>(undefined);
  const runRevision = useRef(0);
  const importController = useRef<AbortController | undefined>(undefined);
  const zipCancellation = useRef<(() => void) | undefined>(undefined);

  const configurationIdentity = useMemo(
    () =>
      JSON.stringify({
        flavour,
        flavourVersion,
        options,
        pattern,
        flags,
        replacement,
      }),
    [flags, flavour, flavourVersion, options, pattern, replacement],
  );
  const previousConfigurationIdentity = useRef(configurationIdentity);

  const totalBytes = useMemo(() => corpusTotalBytes(documents), [documents]);
  const canRun =
    documents.length > 0 &&
    patternAccepted &&
    (operation === "match" || replacementAccepted) &&
    !running &&
    !importing;
  const exactAppliedExport = batch ? canExportAppliedCorpus(batch) : false;
  const selectedResult = batch?.documents.find(
    (result) => result.documentId === selectedDocumentId,
  );

  const cancelRun = (message = "Cancelling corpus processing…") => {
    if (!runController.current) return;
    runController.current.abort();
    engine.current?.cancel();
    setStatus(message);
  };

  const invalidateResults = (message: string) => {
    runRevision.current += 1;
    if (running) cancelRun();
    setBatch(undefined);
    setProgress(undefined);
    setAllowContentExport(false);
    setSelectedDocumentId(undefined);
    setStatus(message);
  };

  useEffect(() => {
    if (previousConfigurationIdentity.current === configurationIdentity) return;
    previousConfigurationIdentity.current = configurationIdentity;
    runRevision.current += 1;
    if (running) {
      runController.current?.abort();
      engine.current?.cancel();
    }
    setBatch(undefined);
    setProgress(undefined);
    setAllowContentExport(false);
    setSelectedDocumentId(undefined);
    setStatus(
      "Pattern, flavour, flags, options or replacement changed; previous corpus results were cleared.",
    );
  }, [configurationIdentity, running]);

  useEffect(() => {
    if (!active && running) {
      runController.current?.abort();
      engine.current?.cancel();
    }
  }, [active, running]);

  useEffect(
    () => () => {
      runController.current?.abort();
      importController.current?.abort();
      zipCancellation.current?.();
      engine.current?.dispose();
      engine.current = undefined;
    },
    [],
  );

  const importFiles = async (files: readonly File[]) => {
    if (files.length === 0) return;
    const controller = new AbortController();
    importController.current?.abort();
    importController.current = controller;
    setImporting(true);
    setStatus(`Reading ${files.length.toLocaleString()} local text files…`);
    try {
      const imported = await readCorpusFiles(
        files,
        documents,
        controller.signal,
        (completed, total, name) =>
          setStatus(`Read ${completed} of ${total}: ${name}`),
      );
      if (controller.signal.aborted) return;
      setDocuments((current) => [...current, ...imported]);
      setBatch(undefined);
      setAllowContentExport(false);
      setStatus(
        `Added ${imported.length.toLocaleString()} local text document${
          imported.length === 1 ? "" : "s"
        }. Nothing was uploaded.`,
      );
    } catch (error) {
      if (controller.signal.aborted) {
        setStatus("File import cancelled.");
      } else {
        setStatus(error instanceof Error ? error.message : String(error));
      }
    } finally {
      if (importController.current === controller) {
        importController.current = undefined;
        setImporting(false);
      }
    }
  };

  const run = async () => {
    if (!canRun) return;
    const revision = runRevision.current + 1;
    runRevision.current = revision;
    const controller = new AbortController();
    runController.current = controller;
    engine.current ??= new EngineSupervisor();
    setRunning(true);
    setBatch(undefined);
    setSelectedDocumentId(undefined);
    setAllowContentExport(false);
    setProgress({
      completedDocuments: 0,
      totalDocuments: documents.length,
      currentDocumentName: documents[0]?.name,
      totalMatches: 0,
    });
    setStatus(
      `Processing ${documents.length.toLocaleString()} documents locally…`,
    );
    const wallTimer = window.setTimeout(() => {
      controller.abort("corpus-wall-time");
      engine.current?.cancel();
    }, DEFAULT_REGEX_LIMITS.maximumCorpusWallTimeMs);
    try {
      const result = await runCorpusBatch(
        documents,
        {
          flavour,
          ...(flavourVersion === undefined ? {} : { flavourVersion }),
          options,
          pattern,
          flags,
          captureMetadata,
          replacement,
          operation,
          subjectMode,
          timeoutMs,
        },
        {
          engine: engine.current,
          signal: controller.signal,
          onProgress: setProgress,
        },
      );
      if (runRevision.current !== revision) return;
      setBatch(result);
      setSelectedDocumentId(result.documents[0]?.documentId);
      setStatus(result.message);
    } catch (error) {
      if (runRevision.current !== revision) return;
      setStatus(error instanceof Error ? error.message : String(error));
    } finally {
      window.clearTimeout(wallTimer);
      if (runController.current === controller) {
        runController.current = undefined;
        setRunning(false);
      }
    }
  };

  return (
    <div className="corpus-workspace">
      <section className="panel corpus-input-panel">
        <header className="panel-heading">
          <div>
            <p className="eyebrow">Ephemeral, local text corpus</p>
            <h2>Corpus documents</h2>
          </div>
          <span>
            {documents.length.toLocaleString()} /{" "}
            {DEFAULT_REGEX_LIMITS.maximumCorpusDocuments.toLocaleString()} ·{" "}
            {corpusMiB(totalBytes)}
          </span>
        </header>
        <div className="corpus-add-controls">
          <button
            type="button"
            className="secondary-button"
            disabled={running || importing}
            onClick={() => fileInput.current?.click()}
          >
            Add text files
          </button>
          <input
            ref={fileInput}
            type="file"
            multiple
            accept={CORPUS_FILE_ACCEPT}
            className="visually-hidden"
            tabIndex={-1}
            aria-label="Add corpus text files"
            onChange={(event) => {
              const files = Array.from(event.target.files ?? []);
              event.target.value = "";
              void importFiles(files);
            }}
          />
          {importing ? (
            <button
              type="button"
              className="secondary-button"
              onClick={() => importController.current?.abort()}
            >
              Cancel import
            </button>
          ) : null}
          <small>
            UTF-8 text only ·{" "}
            {corpusMiB(DEFAULT_REGEX_LIMITS.maximumCorpusDocumentBytes)} per
            document · {corpusMiB(DEFAULT_REGEX_LIMITS.corpusHardBytes)} total
          </small>
        </div>
        <div className="corpus-paste-controls">
          <label>
            <span>Document name</span>
            <input
              type="text"
              value={pastedName}
              maxLength={240}
              disabled={running || importing}
              onChange={(event) => setPastedName(event.target.value)}
            />
          </label>
          <label>
            <span>Pasted corpus text</span>
            <textarea
              value={pastedText}
              rows={6}
              disabled={running || importing}
              onChange={(event) => {
                const value = event.target.value;
                if (
                  utf8ByteLength(value) <=
                  DEFAULT_REGEX_LIMITS.maximumCorpusDocumentBytes
                ) {
                  setPastedText(value);
                }
              }}
              placeholder="Paste one text document here"
            />
          </label>
          <div className="corpus-paste-footer">
            <span>
              {utf8ByteLength(pastedText).toLocaleString()} UTF-8 bytes
            </span>
            <button
              type="button"
              className="secondary-button"
              disabled={running || importing}
              onClick={() => {
                try {
                  const added = createPastedCorpusDocument(
                    pastedName,
                    pastedText,
                    documents,
                  );
                  setDocuments((current) => [...current, added]);
                  setPastedText("");
                  setBatch(undefined);
                  setAllowContentExport(false);
                  setStatus(
                    `Added ${added.name}. Its content remains only in this browser tab.`,
                  );
                } catch (error) {
                  setStatus(
                    error instanceof Error ? error.message : String(error),
                  );
                }
              }}
            >
              Add pasted text
            </button>
          </div>
        </div>
        {documents.length > 0 ? (
          <ul className="corpus-document-list" aria-label="Corpus documents">
            {documents.map((document) => (
              <li key={document.id}>
                <span>
                  <strong>{document.name}</strong>
                  <small>
                    {document.bytes.toLocaleString()} B ·{" "}
                    {document.source === "file" ? "local file" : "pasted text"}
                  </small>
                </span>
                <button
                  type="button"
                  className="icon-button"
                  aria-label={`Remove ${document.name}`}
                  disabled={running || importing}
                  onClick={() => {
                    setDocuments((current) =>
                      current.filter(
                        (candidate) => candidate.id !== document.id,
                      ),
                    );
                    invalidateResults(
                      `${document.name} was removed; previous corpus results were cleared.`,
                    );
                  }}
                >
                  ×
                </button>
              </li>
            ))}
          </ul>
        ) : (
          <p className="empty-state">
            Add one or more local files, or paste a text document.
          </p>
        )}
      </section>

      <section className="panel corpus-run-panel">
        <header className="panel-heading">
          <div>
            <p className="eyebrow">Killable sequential worker jobs</p>
            <h2>Find or apply</h2>
          </div>
        </header>
        <fieldset className="corpus-operation-selector corpus-subject-selector">
          <legend>Subject semantics</legend>
          <label>
            <input
              type="radio"
              name="corpus-subject-mode"
              value="document"
              checked={subjectMode === "document"}
              disabled={running}
              onChange={() => {
                setSubjectMode("document");
                invalidateResults(
                  "Corpus subject semantics changed; previous results were cleared.",
                );
              }}
            />
            <span>
              <strong>Whole document</strong>
              <small>
                Run once per document. Matches may cross line endings when the
                pattern and active flags allow it.
              </small>
            </span>
          </label>
          <label>
            <input
              type="radio"
              name="corpus-subject-mode"
              value="line"
              checked={subjectMode === "line"}
              disabled={running}
              onChange={() => {
                setSubjectMode("line");
                invalidateResults(
                  "Corpus subject semantics changed; previous results were cleared.",
                );
              }}
            />
            <span>
              <strong>Independent lines</strong>
              <small>
                Run separately on every logical line. Anchors see one line at a
                time, matches cannot cross lines, and apply preserves each
                original line separator.
              </small>
            </span>
          </label>
        </fieldset>
        <fieldset className="corpus-operation-selector">
          <legend>Corpus operation</legend>
          <label>
            <input
              type="radio"
              name="corpus-operation"
              value="match"
              checked={operation === "match"}
              disabled={running}
              onChange={() => {
                setOperation("match");
                invalidateResults(
                  "Corpus operation changed; previous results were cleared.",
                );
              }}
            />
            <span>
              <strong>Find matches</strong>
              <small>Count every returned match per document.</small>
            </span>
          </label>
          <label>
            <input
              type="radio"
              name="corpus-operation"
              value="replace"
              checked={operation === "replace"}
              disabled={running}
              onChange={() => {
                setOperation("replace");
                invalidateResults(
                  "Corpus operation changed; previous results were cleared.",
                );
              }}
            />
            <span>
              <strong>Apply replacement</strong>
              <small>
                Create bounded local outputs without modifying sources.
              </small>
            </span>
          </label>
        </fieldset>
        {operation === "replace" ? (
          <label className="stacked-control corpus-replacement-control">
            <span>Replacement template</span>
            <textarea
              value={replacement}
              rows={5}
              maxLength={DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16}
              aria-invalid={!replacementAccepted}
              disabled={running}
              onChange={(event) => onReplacementChange(event.target.value)}
              placeholder="Use $&, $1, or $<name>"
            />
          </label>
        ) : null}
        <div className="corpus-run-summary">
          <p>
            Corpus jobs always scan all matches. An internal iteration flag may
            be added for this run; saved user flags are unchanged.
          </p>
          {subjectMode === "line" ? (
            <p>
              Independent-line mode is bounded to{" "}
              {DEFAULT_REGEX_LIMITS.maximumCorpusLines.toLocaleString()} logical
              lines across the batch.
            </p>
          ) : null}
          <p>
            Documents run one at a time with the selected {timeoutMs} ms
            per-document limit and a{" "}
            {DEFAULT_REGEX_LIMITS.maximumCorpusWallTimeMs / 60_000} minute batch
            wall limit.
          </p>
        </div>
        {!patternAccepted ? (
          <p className="privacy-warning" role="alert">
            Corpus processing is paused until the syntax provider accepts the
            current pattern.
          </p>
        ) : operation === "replace" && !replacementAccepted ? (
          <p className="privacy-warning" role="alert">
            Corpus apply is paused until the replacement template is valid.
          </p>
        ) : null}
        <div className="corpus-run-actions">
          <button
            type="button"
            className="primary-button"
            disabled={!canRun}
            onClick={() => void run()}
          >
            {running
              ? "Processing…"
              : operation === "replace"
                ? "Apply to corpus"
                : "Scan corpus"}
          </button>
          {running ? (
            <button
              type="button"
              className="secondary-button"
              onClick={() => cancelRun()}
            >
              Cancel corpus run
            </button>
          ) : null}
          <button
            type="button"
            className="secondary-button"
            disabled={running || importing || documents.length === 0}
            onClick={() => {
              setDocuments([]);
              invalidateResults("Corpus documents and results were cleared.");
            }}
          >
            Clear corpus
          </button>
        </div>
        {progress ? (
          <div className="corpus-progress">
            <progress
              max={progress.totalDocuments}
              value={progress.completedDocuments}
              aria-label="Corpus processing progress"
            />
            <span>
              {progress.completedDocuments.toLocaleString()} of{" "}
              {progress.totalDocuments.toLocaleString()} documents ·{" "}
              {progress.totalMatches.toLocaleString()} matches
              {progress.currentDocumentName
                ? ` · ${progress.currentDocumentName}`
                : ""}
            </span>
          </div>
        ) : null}
        <p role="status" className="corpus-message">
          {status}
        </p>
      </section>

      <section className="panel corpus-results-panel mode-span">
        <header className="panel-heading">
          <div>
            <p className="eyebrow">Per-document bounded summaries</p>
            <h2>Corpus results</h2>
          </div>
          <span>
            {batch
              ? `${batch.totalMatches.toLocaleString()} matches · ${batch.totalElapsedMs.toFixed(2)} ms engine time`
              : "Not run"}
          </span>
        </header>
        {batch ? (
          <>
            <CorpusResultTable
              batch={batch}
              allowContentExport={allowContentExport}
              selectedDocumentId={selectedDocumentId}
              onInspect={(result) => setSelectedDocumentId(result.documentId)}
            />
            {selectedResult ? (
              <section
                className="corpus-inspector"
                aria-labelledby="corpus-inspector-heading"
              >
                <header>
                  <div>
                    <p className="eyebrow">Bounded local detail</p>
                    <h3 id="corpus-inspector-heading">{selectedResult.name}</h3>
                  </div>
                  <span>
                    {selectedResult.captureSummaries.length.toLocaleString()}{" "}
                    retained group summaries
                  </span>
                </header>
                {selectedResult.output !== undefined ? (
                  <div className="corpus-output-preview">
                    <h4>Replacement preview</h4>
                    <pre>{selectedResult.output.slice(0, 2_000) || "∅"}</pre>
                    <p>
                      Showing{" "}
                      {Math.min(
                        2_000,
                        selectedResult.output.length,
                      ).toLocaleString()}{" "}
                      of {selectedResult.output.length.toLocaleString()} UTF-16
                      units
                      {selectedResult.status !== "complete"
                        ? " from incomplete output"
                        : ""}
                      .
                    </p>
                  </div>
                ) : null}
                {selectedResult.captureSummaries.length > 0 ? (
                  <div className="corpus-capture-scroll">
                    <table
                      aria-label={`Capture summaries for ${selectedResult.name}`}
                    >
                      <thead>
                        <tr>
                          <th scope="col">Group</th>
                          <th scope="col">Participated</th>
                          <th scope="col">Empty</th>
                          <th scope="col">Did not participate</th>
                          <th scope="col">Unavailable / clipped</th>
                          <th scope="col">Bounded samples</th>
                        </tr>
                      </thead>
                      <tbody>
                        {selectedResult.captureSummaries.map((capture) => (
                          <tr key={capture.groupNumber}>
                            <th scope="row">
                              {capture.groupNumber === 0
                                ? "Full match"
                                : `${capture.groupNumber}${
                                    capture.groupName
                                      ? ` · ${capture.groupName}`
                                      : ""
                                  }`}
                            </th>
                            <td>{capture.participated.toLocaleString()}</td>
                            <td>{capture.matchedEmpty.toLocaleString()}</td>
                            <td>
                              {capture.didNotParticipate.toLocaleString()}
                            </td>
                            <td>
                              {(
                                capture.unavailable + capture.truncated
                              ).toLocaleString()}
                            </td>
                            <td>
                              {capture.samples.length > 0 ? (
                                <ul>
                                  {capture.samples.map((sample, index) => (
                                    <li key={`${capture.groupNumber}-${index}`}>
                                      <code>{sample || "∅"}</code>
                                    </li>
                                  ))}
                                </ul>
                              ) : (
                                "—"
                              )}
                              {capture.samplesClipped ? (
                                <small>One or more samples were clipped.</small>
                              ) : null}
                            </td>
                          </tr>
                        ))}
                      </tbody>
                    </table>
                  </div>
                ) : (
                  <p className="empty-state">
                    No match or capture samples were retained for this document.
                  </p>
                )}
              </section>
            ) : null}
            <div className="corpus-export-controls">
              <div>
                <label>
                  <span>Summary format</span>
                  <select
                    value={reportFormat}
                    onChange={(event) =>
                      setReportFormat(event.target.value as CorpusReportFormat)
                    }
                  >
                    <option value="json">JSON</option>
                    <option value="csv">CSV</option>
                    <option value="ndjson">NDJSON</option>
                  </select>
                </label>
                <button
                  type="button"
                  className="secondary-button"
                  onClick={() => {
                    try {
                      const serialized = serializeCorpusReport(
                        batch,
                        {
                          flavour,
                          ...(flavourVersion === undefined
                            ? {}
                            : { flavourVersion }),
                          pattern,
                          flags,
                        },
                        reportFormat,
                      );
                      const mediaType =
                        reportFormat === "csv"
                          ? "text/csv;charset=utf-8"
                          : reportFormat === "ndjson"
                            ? "application/x-ndjson"
                            : "application/json";
                      downloadBlob(
                        new Blob([serialized], { type: mediaType }),
                        `regex-corpus-report.${reportFormat}`,
                      );
                      setStatus(
                        "Downloaded a summary report without source or replacement output content.",
                      );
                    } catch (error) {
                      setStatus(
                        error instanceof Error ? error.message : String(error),
                      );
                    }
                  }}
                >
                  Download summary {reportFormat.toUpperCase()}
                </button>
                <small>
                  Includes names, pattern and diagnostics, but no document
                  contents or applied output.
                </small>
              </div>
              {batch.operation === "replace" ? (
                <>
                  <label className="check-control corpus-content-confirmation">
                    <input
                      type="checkbox"
                      checked={allowContentExport}
                      onChange={(event) =>
                        setAllowContentExport(event.target.checked)
                      }
                    />
                    I understand applied downloads contain corpus content
                  </label>
                  <button
                    type="button"
                    className="primary-button"
                    disabled={
                      !allowContentExport || !exactAppliedExport || exportingZip
                    }
                    title={
                      !exactAppliedExport
                        ? "ZIP export requires every document to complete exactly."
                        : undefined
                    }
                    onClick={() => {
                      try {
                        const pending = createAppliedCorpusZip(batch);
                        zipCancellation.current = pending.cancel;
                        setExportingZip(true);
                        setStatus("Creating applied-output ZIP locally…");
                        void pending.promise
                          .then((archive) => {
                            const archiveBytes = Uint8Array.from(archive);
                            downloadBlob(
                              new Blob([archiveBytes.buffer], {
                                type: "application/zip",
                              }),
                              "regex-corpus-applied.zip",
                            );
                            setStatus(
                              "Downloaded exact applied outputs. Original files were not modified.",
                            );
                          })
                          .catch((error: unknown) =>
                            setStatus(
                              error instanceof Error
                                ? error.message
                                : String(error),
                            ),
                          )
                          .finally(() => {
                            zipCancellation.current = undefined;
                            setExportingZip(false);
                          });
                      } catch (error) {
                        setStatus(
                          error instanceof Error
                            ? error.message
                            : String(error),
                        );
                      }
                    }}
                  >
                    {exportingZip ? "Creating ZIP…" : "Download applied ZIP"}
                  </button>
                </>
              ) : null}
            </div>
            {batch.status !== "complete" ? (
              <p className="privacy-warning" role="alert">
                This batch is {batch.status}. Counts marked with ≥ are lower
                bounds. Applied downloads are disabled for incomplete rows and
                the all-document ZIP is disabled unless every output is exact.
              </p>
            ) : null}
          </>
        ) : (
          <p className="empty-state">
            Run the corpus to create per-document summaries.
          </p>
        )}
      </section>
    </div>
  );
}
