import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { CodeEditor } from "../editors/CodeEditor";
import type { EditorMark } from "../editors/editor.types";
import {
  DEFAULT_REGEX_LIMITS,
  utf8ByteLength,
} from "../regex/execution/request-limits";
import { EngineSupervisor } from "../regex/execution/EngineSupervisor";
import { SyntaxSupervisor } from "../regex/execution/SyntaxSupervisor";
import { WorkerRequestError } from "../regex/execution/WorkerSupervisor";
import type { RegexDiagnostic } from "../regex/model/diagnostics";
import type {
  RegexEngineOptions,
  RegexFlavourId,
  RegexOptionValue,
} from "../regex/model/flavour";
import type {
  ExtractionNode,
  RegexExecutionRequest,
  RegexExecutionResult,
  RegexReplacementResult,
} from "../regex/model/match";
import type {
  NormalizedRegexNode,
  RegexSyntaxResult,
  ReplacementSyntaxResult,
  ReplacementToken,
  SourceRange,
} from "../regex/model/syntax";
import {
  isPatternSyntaxCurrent,
  isPatternSyntaxForInput,
  type PatternSyntaxSnapshot,
} from "../regex/syntax/pattern-syntax-freshness";
import {
  buildReplacementPreview,
  type ReplacementContributionPreview,
} from "../regex/replacement/replacement-preview";
import { buildExtractionTree } from "../regex/extraction/build-extraction-tree";
import {
  buildCaptureRows,
  type CaptureRow,
} from "../regex/extraction/capture-rows";
import { parseListTemplate } from "../regex/list/list-template";
import {
  MAXIMUM_REGEX_TEST_NAME_UTF16,
  MAXIMUM_REGEX_TEST_RESULT_MESSAGE_BYTES,
  MAXIMUM_REGEX_TESTS,
} from "../regex/tests/test-case.types";
import type {
  RegexTestCase,
  RegexTestResult,
} from "../regex/tests/test-case.types";
import {
  boundedTestResultMessage,
  runRegexTest,
} from "../regex/tests/test-runner";
import { chooseReplacementResultDraft } from "../regex/tests/current-result-draft";
import { assertRegexTestSuiteWithinLimit } from "../regex/tests/test-suite.serialization";
import type { RegexProjectV1, WorkspaceMode } from "../project/project.types";
import {
  saveProjectLocally,
  loadMostRecentProject,
} from "../project/persistence";
import { ExplanationTree } from "./ExplanationTree";
import { ExtractionTree } from "./ExtractionTree";
import { CaptureTable } from "./CaptureTable";
import { DiagnosticsPanel } from "./DiagnosticsPanel";
import { ReplacementPanel } from "./ReplacementPanel";
import { ListPanel } from "./ListPanel";
import { UnitTestPanel, type RegexTestDraft } from "./UnitTestPanel";
import { QuickReference } from "./QuickReference";
import { CapabilityPanel } from "./CapabilityPanel";
import { ProjectPanel } from "./ProjectPanel";
import { ModalDialog } from "./ModalDialog";
import { CorpusPanel } from "./CorpusPanel";
import { Pcre2TracePanel } from "./Pcre2TracePanel";
import { AnalysisPanel } from "./AnalysisPanel";
import { ComparisonCodePanel } from "./ComparisonCodePanel";
import { MinimizePanel } from "./MinimizePanel";
import { GenerationPanel } from "./GenerationPanel";
import { PatternFormatterPanel } from "./PatternFormatterPanel";
import type { VerifiedGeneratedCase } from "../regex/generation/generation.types";
import {
  AVAILABLE_REGEX_FLAVOURS,
  defaultRegexOptions,
  resolveRegexFlavourVersion,
} from "../regex/flavours/flavour-registry";

const INITIAL_PATTERN =
  "(?<date>\\d{4}-\\d{2}-\\d{2})\\s+(?<user>[\\p{Letter}._-]+)";
const INITIAL_SUBJECT = `2026-07-24 alice
Invalid row
2025-12-31 Bérénice`;
const INITIAL_REPLACEMENT = "$<user> — $<date>";
const MAXIMUM_PATTERN_EDITOR_MARKS = 2_000;
const MAXIMUM_SUBJECT_EDITOR_MARKS = 2_000;
const MAXIMUM_TEST_SUITE_WALL_TIME_MS = 60_000;
const INITIAL_FLAVOUR = AVAILABLE_REGEX_FLAVOURS.require("ecmascript");

type RunState =
  | { readonly status: "idle"; readonly message: string }
  | { readonly status: "running"; readonly message: string }
  | { readonly status: "ready"; readonly message: string }
  | { readonly status: "timeout"; readonly message: string }
  | { readonly status: "error"; readonly message: string };

function findSmallestNode(
  node: NormalizedRegexNode,
  range: SourceRange,
): NormalizedRegexNode | undefined {
  if (
    node.range.startUtf16 > range.startUtf16 ||
    node.range.endUtf16 < range.endUtf16
  ) {
    return undefined;
  }
  for (const child of node.children) {
    const nested = findSmallestNode(child, range);
    if (nested) return nested;
  }
  return node;
}

function findSmallestExtractionNode(
  nodes: readonly ExtractionNode[],
  range: SourceRange,
): ExtractionNode | undefined {
  const isCollapsed = range.startUtf16 === range.endUtf16;
  let smallest: ExtractionNode | undefined;
  for (const node of nodes) {
    if (!node.range) continue;
    const contains = isCollapsed
      ? node.range.startUtf16 === node.range.endUtf16
        ? node.range.startUtf16 === range.startUtf16
        : node.range.startUtf16 <= range.startUtf16 &&
          range.startUtf16 < node.range.endUtf16
      : node.range.startUtf16 <= range.startUtf16 &&
        node.range.endUtf16 >= range.endUtf16;
    if (!contains) continue;

    const candidate = findSmallestExtractionNode(node.children, range) ?? node;
    if (!candidate.range) continue;
    const candidateLength =
      candidate.range.endUtf16 - candidate.range.startUtf16;
    const smallestLength = smallest?.range
      ? smallest.range.endUtf16 - smallest.range.startUtf16
      : Number.POSITIVE_INFINITY;
    if (
      candidateLength < smallestLength ||
      (candidateLength === smallestLength &&
        candidate.groupNumber !== undefined &&
        smallest?.groupNumber === undefined)
    ) {
      smallest = candidate;
    }
  }
  return smallest;
}

function findExtractionNodeById(
  nodes: readonly ExtractionNode[],
  id: string,
): ExtractionNode | undefined {
  for (const node of nodes) {
    if (node.id === id) return node;
    const nested = findExtractionNodeById(node.children, id);
    if (nested) return nested;
  }
  return undefined;
}

interface PatternMarkSummary {
  readonly marks: readonly EditorMark[];
  readonly renderedTokenMarks: number;
  readonly totalTokenMarks: number;
  readonly truncated: boolean;
}

function syntaxMarks(
  syntax: RegexSyntaxResult | undefined,
  selected: NormalizedRegexNode | undefined,
): PatternMarkSummary {
  const totalTokenMarks = syntax?.tokens.length ?? 0;
  const capturesByNumber = new Map(
    syntax?.captures.map((capture) => [capture.number, capture]) ?? [],
  );
  const tokenMarks =
    syntax?.tokens.slice(0, MAXIMUM_PATTERN_EDITOR_MARKS).map((token) => {
      const capture =
        token.captureNumber === undefined
          ? undefined
          : capturesByNumber.get(token.captureNumber);
      return {
        id: token.id,
        range: token.range,
        className: [
          "cm-syntax-token",
          `cm-token-${token.kind}`,
          token.captureNumber
            ? `cm-capture-${((token.captureNumber - 1) % 8) + 1}`
            : "",
        ]
          .filter(Boolean)
          .join(" "),
        label: capture
          ? `${token.kind.replaceAll("-", " ")} · group ${capture.number}${
              capture.name ? ` · ${capture.name}` : ""
            }`
          : token.kind.replaceAll("-", " "),
      };
    }) ?? [];
  const diagnosticMarks =
    syntax?.diagnostics
      .filter(
        (diagnostic): diagnostic is RegexDiagnostic & { range: SourceRange } =>
          diagnostic.range !== undefined,
      )
      .map((diagnostic) => ({
        id: `diagnostic-${diagnostic.id}`,
        range: diagnostic.range,
        className: `cm-diagnostic-${diagnostic.severity}`,
        label: diagnostic.message,
      })) ?? [];
  return {
    marks: [
      ...tokenMarks,
      ...diagnosticMarks,
      ...(selected
        ? [
            {
              id: "selected-syntax",
              range: selected.range,
              className: "cm-selected-range",
              label: selected.explanation,
            },
          ]
        : []),
    ],
    renderedTokenMarks: tokenMarks.length,
    totalTokenMarks,
    truncated: tokenMarks.length < totalTokenMarks,
  };
}

interface SubjectMarkSummary {
  readonly marks: readonly EditorMark[];
  readonly renderedResultMarks: number;
  readonly totalResultMarks: number;
  readonly truncated: boolean;
}

function subjectMarks(
  execution: RegexExecutionResult | undefined,
  selectedRange: SourceRange | undefined,
): SubjectMarkSummary {
  if (!execution) {
    return {
      marks: [],
      renderedResultMarks: 0,
      totalResultMarks: 0,
      truncated: false,
    };
  }
  const marks: EditorMark[] = [];
  const resultMarkLimit = Math.max(
    0,
    MAXIMUM_SUBJECT_EDITOR_MARKS - (selectedRange ? 1 : 0),
  );
  let totalResultMarks = 0;
  const addResultMark = (mark: EditorMark) => {
    totalResultMarks += 1;
    if (marks.length < resultMarkLimit) marks.push(mark);
  };
  for (const match of execution.matches) {
    addResultMark({
      id: `subject-match-${match.matchNumber}`,
      range: match.range,
      className: "cm-subject-match",
      label: `Match ${match.matchNumber}`,
      zeroWidth: match.range.startUtf16 === match.range.endUtf16,
    });
    for (const capture of match.captures) {
      if (!capture.range) continue;
      addResultMark({
        id: `subject-capture-${match.matchNumber}-${capture.groupNumber}`,
        range: capture.range,
        className: `cm-subject-capture cm-capture-${
          ((capture.groupNumber - 1) % 8) + 1
        }`,
        label: `Match ${match.matchNumber}, capture ${capture.groupNumber}`,
        zeroWidth: capture.range.startUtf16 === capture.range.endUtf16,
      });
    }
  }
  const renderedResultMarks = marks.length;
  if (selectedRange) {
    marks.push({
      id: "selected-subject",
      range: selectedRange,
      className: "cm-selected-range",
      label: "Selected result",
      zeroWidth: selectedRange.startUtf16 === selectedRange.endUtf16,
    });
  }
  return {
    marks,
    renderedResultMarks,
    totalResultMarks,
    truncated: renderedResultMarks < totalResultMarks,
  };
}

let fallbackIdSequence = 0;

function projectId(): string {
  return (
    globalThis.crypto?.randomUUID?.() ??
    `regex-${Date.now()}-${(fallbackIdSequence += 1)}`
  );
}

function testCaseFromDraft(
  id: string,
  enabled: boolean,
  draft: RegexTestDraft,
  configuration: {
    readonly flavour: RegexFlavourId;
    readonly flavourVersion?: string;
    readonly options: RegexEngineOptions;
  },
): RegexTestCase {
  return {
    id,
    name: draft.name,
    enabled,
    flavour: configuration.flavour,
    ...(configuration.flavourVersion === undefined
      ? {}
      : { flavourVersion: configuration.flavourVersion }),
    pattern: draft.pattern,
    flags: draft.flags,
    options: configuration.options,
    scanAll: draft.scanAll,
    subject: draft.subject,
    ...(draft.replacement === undefined
      ? {}
      : { replacement: draft.replacement }),
    expectation: draft.expectation,
    ...(draft.resourceLimits === undefined
      ? {}
      : { resourceLimits: draft.resourceLimits }),
    ...(draft.generation === undefined ? {} : { generation: draft.generation }),
  };
}

function captureGroupLimitMessage(count: number): string {
  return `Pattern defines ${count.toLocaleString()} capture groups, above the ${DEFAULT_REGEX_LIMITS.maximumCaptureGroups.toLocaleString()} group execution limit.`;
}

function engineRejectionMessage(result: RegexExecutionResult): string {
  const detail =
    result.diagnostics.find((diagnostic) => diagnostic.severity === "error")
      ?.message ?? "The execution engine could not compile the pattern.";
  return `The syntax provider accepted the pattern, but the execution engine rejected it: ${detail}`;
}

function failedTestResult(testId: string, message: string): RegexTestResult {
  return {
    testId,
    passed: false,
    elapsedMs: 0,
    message: boundedTestResultMessage(
      message,
      MAXIMUM_REGEX_TEST_RESULT_MESSAGE_BYTES,
    ),
    timedOut: false,
  };
}

export function Workbench() {
  const [flavour, setFlavour] = useState<RegexFlavourId>(INITIAL_FLAVOUR.id);
  const [flavourVersion, setFlavourVersion] = useState(
    INITIAL_FLAVOUR.defaultVersion,
  );
  const [options, setOptions] = useState<RegexEngineOptions>(() =>
    defaultRegexOptions(INITIAL_FLAVOUR),
  );
  const [pattern, setPattern] = useState(INITIAL_PATTERN);
  const [flags, setFlags] = useState<readonly string[]>(
    INITIAL_FLAVOUR.defaultFlags,
  );
  const [subject, setSubject] = useState(INITIAL_SUBJECT);
  const [replacement, setReplacement] = useState(INITIAL_REPLACEMENT);
  const [listTemplate, setListTemplate] = useState("${user} — ${date}");
  const [mode, setMode] = useState<WorkspaceMode>("match");
  const [live, setLive] = useState(true);
  const [scanAll, setScanAll] = useState(false);
  const [timeoutMs, setTimeoutMs] = useState(
    DEFAULT_REGEX_LIMITS.manualExecutionTimeoutMs,
  );
  const [syntaxSnapshot, setSyntaxSnapshot] =
    useState<PatternSyntaxSnapshot<RegexSyntaxResult>>();
  const [replacementSyntax, setReplacementSyntax] =
    useState<ReplacementSyntaxResult>();
  const [execution, setExecution] = useState<RegexExecutionResult>();
  const [replacementResult, setReplacementResult] =
    useState<RegexReplacementResult>();
  const [selectedSyntax, setSelectedSyntax] = useState<NormalizedRegexNode>();
  const [requestedPatternRange, setRequestedPatternRange] =
    useState<SourceRange>();
  const [requestedSubjectRange, setRequestedSubjectRange] =
    useState<SourceRange>();
  const [selectedExtraction, setSelectedExtraction] =
    useState<ExtractionNode>();
  const [selectedCaptureRow, setSelectedCaptureRow] = useState<CaptureRow>();
  const [selectedReplacementToken, setSelectedReplacementToken] =
    useState<ReplacementToken>();
  const [selectedReplacementContribution, setSelectedReplacementContribution] =
    useState<ReplacementContributionPreview>();
  const [requestedOutputRange, setRequestedOutputRange] =
    useState<SourceRange>();
  const [runState, setRunState] = useState<RunState>({
    status: "idle",
    message: "Ready for local execution.",
  });
  const [tests, setTests] = useState<readonly RegexTestCase[]>([]);
  const [testResults, setTestResults] = useState<
    ReadonlyMap<string, RegexTestResult>
  >(new Map());
  const [currentResultDraft, setCurrentResultDraft] =
    useState<RegexTestDraft>();
  const [testsRunning, setTestsRunning] = useState(false);
  const [showReference, setShowReference] = useState(false);
  const [showCapabilities, setShowCapabilities] = useState(false);
  const [showTrace, setShowTrace] = useState(false);
  const [showAnalysis, setShowAnalysis] = useState(false);
  const [showComparison, setShowComparison] = useState(false);
  const [showMinimizer, setShowMinimizer] = useState(false);
  const [showGeneration, setShowGeneration] = useState(false);
  const [showFormatter, setShowFormatter] = useState(false);
  const [showProject, setShowProject] = useState(false);
  const [showSubjectWhitespace, setShowSubjectWhitespace] = useState(false);
  const [corpusSession, setCorpusSession] = useState(0);
  const [subjectCursor, setSubjectCursor] = useState({ line: 1, column: 1 });
  const [projectIdentity, setProjectIdentity] = useState(() => ({
    id: projectId(),
    createdAt: new Date().toISOString(),
  }));
  const patternSyntaxSupervisor = useRef<SyntaxSupervisor | null>(null);
  const replacementSyntaxSupervisor = useRef<SyntaxSupervisor | null>(null);
  const engineSupervisor = useRef<EngineSupervisor | null>(null);
  const testSyntaxSupervisor = useRef<SyntaxSupervisor | null>(null);
  const testEngineSupervisor = useRef<EngineSupervisor | null>(null);
  const syntaxRevision = useRef(0);
  const replacementSyntaxRevision = useRef(0);
  const executionRevision = useRef(0);
  const testRunRevision = useRef(0);
  const liveExecutionTimer = useRef<number | undefined>(undefined);
  const flavourDefinition = AVAILABLE_REGEX_FLAVOURS.require(flavour);
  const activeTestConfiguration = useMemo(
    () => ({ flavour, flavourVersion, options }),
    [flavour, flavourVersion, options],
  );

  const patternSyntaxEngine = () => {
    patternSyntaxSupervisor.current ??= new SyntaxSupervisor(
      "Pattern syntax parser",
      "regex-tools-pattern-syntax",
    );
    return patternSyntaxSupervisor.current;
  };
  const replacementSyntaxEngine = () => {
    replacementSyntaxSupervisor.current ??= new SyntaxSupervisor(
      "Replacement syntax parser",
      "regex-tools-replacement-syntax",
    );
    return replacementSyntaxSupervisor.current;
  };
  const executionEngine = () => {
    engineSupervisor.current ??= new EngineSupervisor();
    return engineSupervisor.current;
  };
  const testSyntaxEngine = () => {
    testSyntaxSupervisor.current ??= new SyntaxSupervisor();
    return testSyntaxSupervisor.current;
  };
  const testExecutionEngine = () => {
    testEngineSupervisor.current ??= new EngineSupervisor();
    return testEngineSupervisor.current;
  };

  const clearExecutionResults = useCallback(() => {
    setExecution(undefined);
    setReplacementResult(undefined);
    setSelectedExtraction(undefined);
    setSelectedCaptureRow(undefined);
    setRequestedSubjectRange(undefined);
    setSelectedReplacementContribution(undefined);
    setRequestedOutputRange(undefined);
  }, []);

  const invalidateExecution = (preserveCurrentResult = false) => {
    executionRevision.current += 1;
    engineSupervisor.current?.cancel();
    clearExecutionResults();
    if (!preserveCurrentResult) setCurrentResultDraft(undefined);
    setRunState({
      status: "idle",
      message: "Inputs changed; previous results were cleared.",
    });
  };

  const invalidatePatternSyntax = () => {
    syntaxRevision.current += 1;
    replacementSyntaxRevision.current += 1;
    patternSyntaxSupervisor.current?.cancel();
    replacementSyntaxSupervisor.current?.cancel();
    setSyntaxSnapshot(undefined);
    setReplacementSyntax(undefined);
    setSelectedSyntax(undefined);
    setSelectedReplacementToken(undefined);
    setSelectedReplacementContribution(undefined);
    setRequestedOutputRange(undefined);
  };

  const changePattern = (value: string) => {
    invalidateExecution();
    invalidatePatternSyntax();
    setRequestedPatternRange(undefined);
    setPattern(value);
  };

  const changeSubject = (value: string) => {
    invalidateExecution();
    setSubject(value);
  };

  const changeReplacement = (value: string) => {
    invalidateExecution();
    replacementSyntaxRevision.current += 1;
    replacementSyntaxSupervisor.current?.cancel();
    setReplacementSyntax(undefined);
    setSelectedReplacementToken(undefined);
    setSelectedReplacementContribution(undefined);
    setRequestedOutputRange(undefined);
    setReplacement(value);
  };

  useEffect(
    () => () => {
      if (liveExecutionTimer.current !== undefined) {
        window.clearTimeout(liveExecutionTimer.current);
        liveExecutionTimer.current = undefined;
      }
      patternSyntaxSupervisor.current?.dispose();
      replacementSyntaxSupervisor.current?.dispose();
      engineSupervisor.current?.dispose();
      testSyntaxSupervisor.current?.dispose();
      testEngineSupervisor.current?.dispose();
      patternSyntaxSupervisor.current = null;
      replacementSyntaxSupervisor.current = null;
      engineSupervisor.current = null;
      testSyntaxSupervisor.current = null;
      testEngineSupervisor.current = null;
    },
    [],
  );

  const syntaxRequest = useMemo(() => {
    const definition = AVAILABLE_REGEX_FLAVOURS.require(flavour);
    const version = resolveRegexFlavourVersion(
      flavourVersion,
      definition,
      "Selected flavour version",
    ).definition;
    return {
      flavour,
      flavourVersion: version.syntaxVersion,
      pattern,
      flags,
      options,
    };
  }, [flags, flavour, flavourVersion, options, pattern]);
  const syntax = isPatternSyntaxForInput(syntaxSnapshot, syntaxRequest)
    ? syntaxSnapshot.result
    : undefined;

  useEffect(() => {
    const currentRevision = syntaxRevision.current;
    const timer = window.setTimeout(() => {
      if (pattern.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
        if (syntaxRevision.current !== currentRevision) return;
        setSyntaxSnapshot(undefined);
        setRunState({
          status: "error",
          message: `Pattern exceeds the ${DEFAULT_REGEX_LIMITS.patternHardLengthUtf16.toLocaleString()} UTF-16 unit limit.`,
        });
        return;
      }
      void patternSyntaxEngine()
        .parsePattern(syntaxRequest)
        .then((result) => {
          if (syntaxRevision.current !== currentRevision) return;
          setSyntaxSnapshot({
            revision: currentRevision,
            request: syntaxRequest,
            result,
          });
          setSelectedSyntax((selected) =>
            selected
              ? findSmallestNode(result.root, selected.range)
              : undefined,
          );
          if (!result.accepted) {
            clearExecutionResults();
            setRunState({
              status: "error",
              message:
                "Execution is paused until both syntax provider and engine accept the pattern.",
            });
          } else if (
            result.captures.length > DEFAULT_REGEX_LIMITS.maximumCaptureGroups
          ) {
            clearExecutionResults();
            setRunState({
              status: "error",
              message: captureGroupLimitMessage(result.captures.length),
            });
          }
        })
        .catch((error: unknown) => {
          if (syntaxRevision.current !== currentRevision) return;
          if (
            error instanceof WorkerRequestError &&
            error.kind === "cancelled"
          ) {
            return;
          }
          clearExecutionResults();
          setRunState({
            status: "error",
            message: error instanceof Error ? error.message : String(error),
          });
        });
    }, DEFAULT_REGEX_LIMITS.liveParseDebounceMs);
    return () => window.clearTimeout(timer);
  }, [clearExecutionResults, pattern, syntaxRequest]);

  useEffect(() => {
    if (!syntax) return;
    const currentRevision = ++replacementSyntaxRevision.current;
    void replacementSyntaxEngine()
      .parseReplacement({
        flavour,
        replacement,
        captureMetadata: syntax.captures,
      })
      .then((result) => {
        if (replacementSyntaxRevision.current !== currentRevision) return;
        setReplacementSyntax(result);
      })
      .catch((error: unknown) => {
        if (replacementSyntaxRevision.current !== currentRevision) return;
        if (
          !(error instanceof WorkerRequestError) ||
          error.kind !== "cancelled"
        ) {
          console.warn("Replacement parsing failed", error);
        }
      });
  }, [flavour, replacement, syntax]);

  const subjectBytes = useMemo(() => utf8ByteLength(subject), [subject]);

  const makeRequest = useCallback(
    (): RegexExecutionRequest => ({
      flavour,
      flavourVersion,
      pattern,
      flags,
      options,
      subject,
      captureMetadata: syntax?.captures ?? [],
      scanAll,
      maximumMatches: DEFAULT_REGEX_LIMITS.maximumMatches,
      maximumCaptureRows: DEFAULT_REGEX_LIMITS.maximumCaptureRows,
    }),
    [
      flags,
      flavour,
      flavourVersion,
      options,
      pattern,
      scanAll,
      subject,
      syntax?.captures,
    ],
  );

  const run = useCallback(
    async (kind: "live" | "manual") => {
      if (kind === "manual" && liveExecutionTimer.current !== undefined) {
        window.clearTimeout(liveExecutionTimer.current);
        liveExecutionTimer.current = undefined;
      }
      clearExecutionResults();
      setCurrentResultDraft(undefined);
      if (
        !syntax?.accepted ||
        !isPatternSyntaxCurrent(
          syntaxSnapshot,
          syntaxRevision.current,
          syntaxRequest,
        )
      ) {
        setRunState({
          status: "error",
          message:
            "Execution is paused until both syntax provider and engine accept the pattern.",
        });
        return;
      }
      if (mode === "replace" && replacementSyntax?.accepted !== true) {
        setRunState({
          status: "error",
          message:
            "Execution is paused until the replacement template is parsed and within its input limit.",
        });
        return;
      }
      if (syntax.captures.length > DEFAULT_REGEX_LIMITS.maximumCaptureGroups) {
        setRunState({
          status: "error",
          message: captureGroupLimitMessage(syntax.captures.length),
        });
        return;
      }
      if (subjectBytes > DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes) {
        setRunState({
          status: "error",
          message: `Subject exceeds the ${DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes.toLocaleString()} byte limit.`,
        });
        return;
      }
      const requestRevision = ++executionRevision.current;
      const limit =
        kind === "live"
          ? DEFAULT_REGEX_LIMITS.liveExecutionTimeoutMs
          : timeoutMs;
      setRunState({
        status: "running",
        message: `Running in a worker with a ${limit} ms limit…`,
      });
      try {
        let elapsedMs = 0;
        let resultDraftNotice: string | undefined;
        if (mode === "replace") {
          const result = await executionEngine().replace(
            {
              ...makeRequest(),
              replacement,
              maximumOutputBytes:
                DEFAULT_REGEX_LIMITS.maximumReplacementOutputBytes,
            },
            limit,
          );
          if (executionRevision.current !== requestRevision) return;
          setExecution(result.execution);
          if (!result.execution.accepted) {
            setReplacementResult(undefined);
            setRunState({
              status: "error",
              message: engineRejectionMessage(result.execution),
            });
            return;
          }
          setReplacementResult(result);
          const commonDraft = {
            name: "Current replacement result",
            pattern,
            flags,
            scanAll,
            subject,
          } as const;
          const decision = chooseReplacementResultDraft({
            output: result.output,
            resultTruncated: result.truncated,
            matchCount: result.execution.matches.length,
            canStore: (expectation) => {
              if (tests.length >= MAXIMUM_REGEX_TESTS) return false;
              const candidate: RegexTestDraft = {
                ...commonDraft,
                ...(expectation.kind === "replacement" ? { replacement } : {}),
                expectation,
              };
              try {
                assertRegexTestSuiteWithinLimit([
                  ...tests,
                  testCaseFromDraft(
                    "current-result-size-check",
                    true,
                    candidate,
                    activeTestConfiguration,
                  ),
                ]);
                return true;
              } catch {
                return false;
              }
            },
          });
          const resultDraft: RegexTestDraft | undefined = decision.expectation
            ? {
                ...commonDraft,
                name:
                  decision.expectation.kind === "replacement"
                    ? "Current replacement result"
                    : "Current match result",
                ...(decision.expectation.kind === "replacement"
                  ? { replacement }
                  : {}),
                expectation: decision.expectation,
              }
            : undefined;
          resultDraftNotice = decision.notice;
          setCurrentResultDraft(resultDraft);
          elapsedMs = result.execution.elapsedMs;
        } else {
          const result = await executionEngine().execute(makeRequest(), limit);
          if (executionRevision.current !== requestRevision) return;
          setExecution(result);
          if (!result.accepted) {
            setRunState({
              status: "error",
              message: engineRejectionMessage(result),
            });
            return;
          }
          setCurrentResultDraft(
            !result.truncated
              ? {
                  name: "Current match result",
                  pattern,
                  flags,
                  scanAll,
                  subject,
                  expectation: {
                    kind: "match-count",
                    count: result.matches.length,
                  },
                }
              : result.matches.length > 0
                ? {
                    name: "Current match result",
                    pattern,
                    flags,
                    scanAll,
                    subject,
                    expectation: { kind: "should-match" },
                  }
                : undefined,
          );
          elapsedMs = result.elapsedMs;
        }
        setRunState({
          status: "ready",
          message: `Completed locally in ${elapsedMs.toFixed(2)} ms.${
            resultDraftNotice ? ` ${resultDraftNotice}` : ""
          }`,
        });
      } catch (error) {
        if (executionRevision.current !== requestRevision) return;
        clearExecutionResults();
        const timedOut =
          error instanceof WorkerRequestError && error.kind === "timeout";
        setRunState({
          status: timedOut ? "timeout" : "error",
          message: timedOut
            ? `${error.message}. The worker was terminated and will be recreated for the next run.`
            : error instanceof Error
              ? error.message
              : String(error),
        });
      }
    },
    [
      activeTestConfiguration,
      clearExecutionResults,
      makeRequest,
      mode,
      replacement,
      replacementSyntax?.accepted,
      scanAll,
      subjectBytes,
      subject,
      syntax?.accepted,
      syntax?.captures.length,
      syntaxSnapshot,
      syntaxRequest,
      flags,
      pattern,
      tests,
      timeoutMs,
    ],
  );

  const liveWithinLimits =
    pattern.length <= DEFAULT_REGEX_LIMITS.patternSoftLengthUtf16 &&
    subjectBytes <= DEFAULT_REGEX_LIMITS.interactiveSubjectSoftBytes;

  useEffect(() => {
    if (
      !live ||
      !liveWithinLimits ||
      !syntax?.accepted ||
      mode === "tests" ||
      mode === "corpus" ||
      (mode === "replace" && replacementSyntax?.accepted !== true)
    ) {
      return;
    }
    const timer = window.setTimeout(() => {
      if (liveExecutionTimer.current === timer) {
        liveExecutionTimer.current = undefined;
      }
      void run("live");
    }, DEFAULT_REGEX_LIMITS.liveExecutionDebounceMs);
    liveExecutionTimer.current = timer;
    return () => {
      window.clearTimeout(timer);
      if (liveExecutionTimer.current === timer) {
        liveExecutionTimer.current = undefined;
      }
    };
  }, [
    live,
    liveWithinLimits,
    mode,
    replacement,
    replacementSyntax?.accepted,
    run,
    syntax?.accepted,
  ]);

  const extraction = useMemo(
    () => buildExtractionTree(execution?.matches ?? [], syntax?.captures ?? []),
    [execution?.matches, syntax?.captures],
  );
  const captureRows = useMemo(
    () => buildCaptureRows(execution?.matches ?? [], subject),
    [execution?.matches, subject],
  );
  const repeatedCaptures = useMemo(
    () =>
      new Set(
        syntax?.captures
          .filter((capture) => capture.repeated)
          .map((capture) => capture.number) ?? [],
      ),
    [syntax?.captures],
  );
  const selectedSubjectRange = useMemo(
    () =>
      selectedCaptureRow?.start === undefined
        ? selectedExtraction?.range
        : {
            startUtf16: selectedCaptureRow.start,
            endUtf16: selectedCaptureRow.end ?? selectedCaptureRow.start,
          },
    [selectedCaptureRow, selectedExtraction?.range],
  );
  const subjectMarkSummary = useMemo(
    () => subjectMarks(execution, selectedSubjectRange),
    [execution, selectedSubjectRange],
  );
  const patternMarkSummary = useMemo(
    () => syntaxMarks(syntax, selectedSyntax),
    [selectedSyntax, syntax],
  );
  const replacementPreview = useMemo(
    () =>
      replacementSyntax?.accepted === true && replacementResult
        ? buildReplacementPreview(replacementSyntax, replacementResult, subject)
        : undefined,
    [replacementResult, replacementSyntax, subject],
  );
  const diagnostics = [
    ...(syntax?.diagnostics ?? []),
    ...(replacementSyntax?.diagnostics ?? []),
    ...(execution?.diagnostics ?? []),
  ];
  const currentProject: RegexProjectV1 = {
    schemaVersion: 1,
    id: projectIdentity.id,
    name: "Regex workbench",
    createdAt: projectIdentity.createdAt,
    updatedAt: new Date().toISOString(),
    flavour,
    flavourVersion,
    pattern,
    flags,
    options,
    replacement,
    mode,
    testText: { included: true, value: subject },
    listTemplate: parseListTemplate(listTemplate),
    tests,
    ui: { live, scanAll },
  };

  const selectExtraction = (
    node: ExtractionNode,
    requestEditorSelection = true,
  ) => {
    setSelectedExtraction(node);
    const matchNumber = /^match-(\d+)(?:-|$)/u.exec(node.id)?.[1];
    const captureRow =
      node.groupNumber === undefined
        ? undefined
        : captureRows.find(
            (row) =>
              row.groupNumber === node.groupNumber &&
              (matchNumber === undefined ||
                row.matchNumber === Number(matchNumber)) &&
              (node.range === undefined ||
                (row.start === node.range.startUtf16 &&
                  row.end === node.range.endUtf16)),
          );
    setSelectedCaptureRow(captureRow);
    setRequestedSubjectRange(
      requestEditorSelection && node.range
        ? {
            startUtf16: node.range.startUtf16,
            endUtf16: node.range.endUtf16,
          }
        : undefined,
    );
    if (node.groupNumber !== undefined) {
      const definition = syntax?.captures.find(
        (capture) => capture.number === node.groupNumber,
      );
      if (definition && syntax) {
        const selected = findSmallestNode(syntax.root, definition.range);
        setSelectedSyntax(selected);
        setRequestedPatternRange(
          selected
            ? {
                startUtf16: selected.range.startUtf16,
                endUtf16: selected.range.endUtf16,
              }
            : undefined,
        );
      }
    } else {
      setSelectedSyntax(syntax?.root);
      setRequestedPatternRange(syntax?.root.range);
    }
  };

  const selectSyntaxNode = (
    node: NormalizedRegexNode,
    requestEditorSelection = true,
  ) => {
    setSelectedSyntax(node);
    setRequestedPatternRange(
      requestEditorSelection
        ? {
            startUtf16: node.range.startUtf16,
            endUtf16: node.range.endUtf16,
          }
        : undefined,
    );

    const enclosingCapture = syntax?.captures
      .filter(
        (capture) =>
          capture.range.startUtf16 <= node.range.startUtf16 &&
          capture.range.endUtf16 >= node.range.endUtf16,
      )
      .sort(
        (left, right) =>
          left.range.endUtf16 -
          left.range.startUtf16 -
          (right.range.endUtf16 - right.range.startUtf16),
      )[0];
    const groupNumber = node.capture?.number ?? enclosingCapture?.number;
    if (groupNumber === undefined) {
      setSelectedExtraction(undefined);
      setSelectedCaptureRow(undefined);
      setRequestedSubjectRange(undefined);
      return;
    }

    const selectedMatchNumber =
      selectedCaptureRow?.matchNumber ??
      Number(/^match-(\d+)(?:-|$)/u.exec(selectedExtraction?.id ?? "")?.[1]);
    const matchingRows = captureRows.filter(
      (row) => row.groupNumber === groupNumber,
    );
    const captureRow =
      matchingRows.find(
        (row) =>
          row.matchNumber === selectedMatchNumber && row.start !== undefined,
      ) ??
      matchingRows.find((row) => row.start !== undefined) ??
      matchingRows.find((row) => row.matchNumber === selectedMatchNumber) ??
      matchingRows[0];
    const extractionNode = captureRow
      ? findExtractionNodeById(
          extraction,
          `match-${captureRow.matchNumber}-capture-${groupNumber}`,
        )
      : undefined;

    setSelectedCaptureRow(captureRow);
    setSelectedExtraction(extractionNode);
    setRequestedSubjectRange(
      captureRow?.start === undefined
        ? undefined
        : {
            startUtf16: captureRow.start,
            endUtf16: captureRow.end ?? captureRow.start,
          },
    );
  };

  const selectReplacementContribution = (
    contribution: ReplacementContributionPreview,
  ) => {
    setSelectedReplacementContribution(contribution);
    setSelectedReplacementToken(contribution.token);
    setRequestedOutputRange(contribution.outputRange);

    const match = execution?.matches.find(
      (candidate) => candidate.matchNumber === contribution.matchNumber,
    );
    const capture =
      contribution.token.captureNumber !== undefined
        ? match?.captures.find(
            (candidate) =>
              candidate.groupNumber === contribution.token.captureNumber,
          )
        : contribution.token.captureName !== undefined
          ? (match?.captures.find(
              (candidate) =>
                candidate.groupName === contribution.token.captureName &&
                candidate.status !== "did-not-participate",
            ) ??
            match?.captures.find(
              (candidate) =>
                candidate.groupName === contribution.token.captureName,
            ))
          : undefined;
    const extractionNode = findExtractionNodeById(
      extraction,
      capture
        ? `match-${contribution.matchNumber}-capture-${capture.groupNumber}`
        : `match-${contribution.matchNumber}`,
    );
    if (extractionNode) {
      selectExtraction(extractionNode);
    } else {
      setSelectedExtraction(undefined);
      setSelectedCaptureRow(undefined);
      setRequestedSubjectRange(match?.range);
    }
  };

  const runTests = async (selectedIds?: readonly string[]) => {
    const selected =
      selectedIds === undefined ? undefined : new Set(selectedIds);
    const testsToRun = tests.filter(
      (candidate) =>
        candidate.enabled &&
        (selected === undefined || selected.has(candidate.id)),
    );
    if (testsToRun.length === 0) return;
    const runRevision = ++testRunRevision.current;
    setLive(false);
    setTestsRunning(true);
    const next =
      selected === undefined
        ? new Map<string, RegexTestResult>()
        : new Map(testResults);
    for (const test of testsToRun) next.delete(test.id);
    setTestResults(new Map(next));
    const deadline = performance.now() + MAXIMUM_TEST_SUITE_WALL_TIME_MS;
    let activeIndex = 0;
    const stopAtSuiteLimit = (fromIndex: number) => {
      if (testRunRevision.current !== runRevision) return;
      testRunRevision.current += 1;
      testSyntaxSupervisor.current?.cancel();
      testEngineSupervisor.current?.cancel();
      for (const remaining of testsToRun.slice(fromIndex)) {
        next.set(
          remaining.id,
          failedTestResult(
            remaining.id,
            `Suite stopped at the ${MAXIMUM_TEST_SUITE_WALL_TIME_MS / 1_000} second aggregate wall-time limit.`,
          ),
        );
      }
      setTestResults(new Map(next));
      setTestsRunning(false);
    };
    const suiteDeadlineTimer = window.setTimeout(() => {
      stopAtSuiteLimit(activeIndex);
    }, MAXIMUM_TEST_SUITE_WALL_TIME_MS);
    try {
      for (const [index, test] of testsToRun.entries()) {
        activeIndex = index;
        if (testRunRevision.current !== runRevision) return;
        if (performance.now() >= deadline) {
          stopAtSuiteLimit(index);
          return;
        }
        try {
          if (
            utf8ByteLength(test.subject) >
            DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes
          ) {
            next.set(
              test.id,
              failedTestResult(
                test.id,
                `Test subject exceeds the ${DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes.toLocaleString()} byte limit.`,
              ),
            );
            setTestResults(new Map(next));
            continue;
          }
          const parsed = await testSyntaxEngine().parsePattern({
            flavour: test.flavour,
            ...(test.flavourVersion === undefined
              ? {}
              : { flavourVersion: test.flavourVersion }),
            pattern: test.pattern,
            flags: test.flags,
            options: test.options,
          });
          if (testRunRevision.current !== runRevision) return;
          if (!parsed.accepted) {
            next.set(
              test.id,
              failedTestResult(
                test.id,
                parsed.diagnostics[0]?.message ?? "Invalid test pattern.",
              ),
            );
          } else if (
            parsed.captures.length > DEFAULT_REGEX_LIMITS.maximumCaptureGroups
          ) {
            next.set(
              test.id,
              failedTestResult(
                test.id,
                captureGroupLimitMessage(parsed.captures.length),
              ),
            );
          } else {
            next.set(
              test.id,
              await runRegexTest(
                test,
                parsed.captures,
                testExecutionEngine(),
                timeoutMs,
                DEFAULT_REGEX_LIMITS.maximumMatches,
                DEFAULT_REGEX_LIMITS.maximumCaptureRows,
                DEFAULT_REGEX_LIMITS.maximumReplacementOutputBytes,
                MAXIMUM_REGEX_TEST_RESULT_MESSAGE_BYTES,
              ),
            );
          }
        } catch (error) {
          if (testRunRevision.current !== runRevision) return;
          next.set(
            test.id,
            failedTestResult(
              test.id,
              error instanceof Error ? error.message : String(error),
            ),
          );
        }
        if (testRunRevision.current !== runRevision) return;
        setTestResults(new Map(next));
      }
    } finally {
      window.clearTimeout(suiteDeadlineTimer);
      if (testRunRevision.current === runRevision) setTestsRunning(false);
    }
  };

  const cancelTestRun = () => {
    testRunRevision.current += 1;
    testSyntaxSupervisor.current?.cancel();
    testEngineSupervisor.current?.cancel();
    setTestsRunning(false);
  };

  const discardTestResult = (id: string) => {
    setTestResults((current) => {
      if (!current.has(id)) return current;
      const next = new Map(current);
      next.delete(id);
      return next;
    });
  };

  const addTest = (draft: RegexTestDraft) => {
    if (tests.length >= MAXIMUM_REGEX_TESTS) {
      throw new Error(
        `A suite is limited to ${MAXIMUM_REGEX_TESTS.toLocaleString()} tests.`,
      );
    }
    const next = [
      ...tests,
      testCaseFromDraft(projectId(), true, draft, activeTestConfiguration),
    ];
    assertRegexTestSuiteWithinLimit(next);
    setTests(next);
  };

  const addGeneratedTests = (
    generatedCases: readonly VerifiedGeneratedCase[],
  ) => {
    if (generatedCases.length === 0) return;
    if (tests.length + generatedCases.length > MAXIMUM_REGEX_TESTS) {
      throw new Error(
        `Adding ${generatedCases.length.toLocaleString()} generated cases would exceed the ${MAXIMUM_REGEX_TESTS.toLocaleString()} test limit.`,
      );
    }
    const appended = generatedCases.map((candidate) =>
      testCaseFromDraft(
        projectId(),
        true,
        {
          name: candidate.name.slice(0, MAXIMUM_REGEX_TEST_NAME_UTF16),
          pattern,
          flags,
          scanAll,
          subject: candidate.subject,
          expectation: { kind: candidate.expectation },
          generation: candidate.provenance,
        },
        activeTestConfiguration,
      ),
    );
    const next = [...tests, ...appended];
    assertRegexTestSuiteWithinLimit(next);
    setTests(next);
  };

  const updateTest = (id: string, draft: RegexTestDraft) => {
    const next = tests.map((test) =>
      test.id === id
        ? testCaseFromDraft(test.id, test.enabled, draft, {
            flavour: test.flavour,
            ...(test.flavourVersion === undefined
              ? {}
              : { flavourVersion: test.flavourVersion }),
            options: test.options,
          })
        : test,
    );
    assertRegexTestSuiteWithinLimit(next);
    setTests(next);
    discardTestResult(id);
  };

  const cloneTest = (id: string) => {
    if (tests.length >= MAXIMUM_REGEX_TESTS) {
      throw new Error(
        `A suite is limited to ${MAXIMUM_REGEX_TESTS.toLocaleString()} tests.`,
      );
    }
    const source = tests.find((test) => test.id === id);
    if (!source) return;
    const suffix = " copy";
    const cloneName = `${source.name.slice(
      0,
      MAXIMUM_REGEX_TEST_NAME_UTF16 - suffix.length,
    )}${suffix}`;
    const next = [
      ...tests,
      {
        ...source,
        id: projectId(),
        name: cloneName,
      },
    ];
    assertRegexTestSuiteWithinLimit(next);
    setTests(next);
  };

  const importTests = (imported: readonly RegexTestCase[]) => {
    if (tests.length + imported.length > MAXIMUM_REGEX_TESTS) {
      throw new Error(
        `Import would exceed the ${MAXIMUM_REGEX_TESTS.toLocaleString()} test limit.`,
      );
    }
    const ids = new Set(tests.map((test) => test.id));
    const appended = imported.map((test) => {
      let id = test.id;
      while (ids.has(id)) id = projectId();
      ids.add(id);
      return id === test.id ? test : { ...test, id };
    });
    const next = [...tests, ...appended];
    assertRegexTestSuiteWithinLimit(next);
    setTests(next);
  };

  const importProject = (project: RegexProjectV1) => {
    const importedFlavour = AVAILABLE_REGEX_FLAVOURS.require(project.flavour);
    invalidatePatternSyntax();
    executionRevision.current += 1;
    engineSupervisor.current?.cancel();
    cancelTestRun();
    setFlavour(importedFlavour.id);
    setFlavourVersion(
      resolveRegexFlavourVersion(
        project.flavourVersion,
        importedFlavour,
        "Project flavour version",
      ).definition.value,
    );
    setOptions(project.options);
    setPattern(project.pattern);
    setProjectIdentity({ id: project.id, createdAt: project.createdAt });
    setFlags(project.flags);
    setSubject(
      project.testText?.included ? (project.testText.value ?? "") : "",
    );
    setReplacement(project.replacement ?? "");
    setListTemplate(project.listTemplate?.source ?? "$0");
    setMode(project.mode);
    setTests(project.tests);
    setCorpusSession((current) => current + 1);
    setCurrentResultDraft(undefined);
    setTestResults(new Map());
    setLive(false);
    setScanAll(project.ui?.scanAll ?? false);
    clearExecutionResults();
    setSelectedSyntax(undefined);
    setRequestedPatternRange(undefined);
    setSelectedReplacementToken(undefined);
    setRunState({
      status: "idle",
      message: "Project imported and validated. Review it, then run manually.",
    });
  };

  const changeFlavour = (value: string) => {
    const next = AVAILABLE_REGEX_FLAVOURS.parse(value, "Selected flavour");
    if (next.id === flavour) return;
    invalidateExecution();
    invalidatePatternSyntax();
    cancelTestRun();
    setFlavour(next.id);
    setFlavourVersion(next.defaultVersion);
    setOptions(defaultRegexOptions(next));
    setFlags(next.defaultFlags);
  };

  const changeFlavourVersion = (value: string) => {
    if (
      value === flavourVersion ||
      !flavourDefinition.versions.some((version) => version.value === value)
    ) {
      return;
    }
    invalidateExecution();
    invalidatePatternSyntax();
    setFlavourVersion(value);
  };

  const changeEngineOption = (name: string, value: RegexOptionValue) => {
    const definition = flavourDefinition.options.find(
      (candidate) => candidate.name === name,
    );
    if (!definition || options[name] === value) return;
    if (definition.kind === "boolean" && typeof value !== "boolean") return;
    if (
      definition.kind === "integer" &&
      (typeof value !== "number" ||
        !Number.isSafeInteger(value) ||
        value < definition.minimum ||
        value > definition.maximum)
    )
      return;
    if (
      definition.kind === "string" &&
      (typeof value !== "string" ||
        value.length > definition.maximumLength ||
        (definition.values !== undefined && !definition.values.includes(value)))
    )
      return;
    invalidateExecution();
    invalidatePatternSyntax();
    setOptions((current) => ({ ...current, [name]: value }));
  };

  return (
    <main className="regex-application">
      <section className="command-bar" aria-label="Regex execution controls">
        <label>
          <span>Flavour</span>
          <select
            value={flavour}
            disabled={AVAILABLE_REGEX_FLAVOURS.definitions.length < 2}
            onChange={(event) => changeFlavour(event.target.value)}
          >
            {AVAILABLE_REGEX_FLAVOURS.definitions.map((definition) => (
              <option key={definition.id} value={definition.id}>
                {definition.label}
              </option>
            ))}
          </select>
        </label>
        <label>
          <span>Version</span>
          <select
            value={flavourVersion}
            disabled={flavourDefinition.versions.length < 2}
            onChange={(event) => changeFlavourVersion(event.target.value)}
          >
            {flavourDefinition.versions.map((version) => (
              <option key={version.value} value={version.value}>
                {version.label}
              </option>
            ))}
          </select>
        </label>
        <fieldset className="flag-selector">
          <legend>Flags</legend>
          {flavourDefinition.flags.map((flag) => (
            <label key={flag.value} title={flag.label}>
              <input
                type="checkbox"
                aria-label={`${flag.label} (${flag.value})`}
                checked={flags.includes(flag.value)}
                onChange={(event) => {
                  invalidateExecution();
                  invalidatePatternSyntax();
                  setFlags((current) => {
                    const next = new Set(current);
                    if (event.target.checked) {
                      next.add(flag.value);
                      for (const group of flavourDefinition.mutuallyExclusiveFlags ??
                        []) {
                        if (!group.includes(flag.value)) continue;
                        for (const incompatible of group) {
                          if (incompatible !== flag.value) {
                            next.delete(incompatible);
                          }
                        }
                      }
                    } else {
                      next.delete(flag.value);
                    }
                    return flavourDefinition.flags
                      .map((definition) => definition.value)
                      .filter((value) => next.has(value));
                  });
                }}
              />
              <span>{flag.value}</span>
            </label>
          ))}
        </fieldset>
        {flavourDefinition.options.length > 0 ? (
          <fieldset className="engine-option-selector">
            <legend>Engine options</legend>
            {flavourDefinition.options.map((option) => (
              <label key={option.name}>
                <span>{option.label}</span>
                {option.kind === "integer" ? (
                  <input
                    type="number"
                    value={Number(options[option.name])}
                    min={option.minimum}
                    max={option.maximum}
                    step={1}
                    onChange={(event) =>
                      changeEngineOption(
                        option.name,
                        Number(event.target.value),
                      )
                    }
                  />
                ) : option.kind === "boolean" ? (
                  <input
                    type="checkbox"
                    checked={options[option.name] === true}
                    onChange={(event) =>
                      changeEngineOption(option.name, event.target.checked)
                    }
                  />
                ) : option.values ? (
                  <select
                    value={String(options[option.name])}
                    onChange={(event) =>
                      changeEngineOption(option.name, event.target.value)
                    }
                  >
                    {option.values.map((value) => (
                      <option key={value} value={value}>
                        {value}
                      </option>
                    ))}
                  </select>
                ) : (
                  <input
                    type="text"
                    value={String(options[option.name])}
                    maxLength={option.maximumLength}
                    onChange={(event) =>
                      changeEngineOption(option.name, event.target.value)
                    }
                  />
                )}
              </label>
            ))}
          </fieldset>
        ) : null}
        <label className="check-control live-control">
          <input
            type="checkbox"
            checked={live}
            onChange={(event) => setLive(event.target.checked)}
          />
          Live
        </label>
        <label className="check-control">
          <input
            type="checkbox"
            checked={scanAll}
            onChange={(event) => {
              invalidateExecution();
              setScanAll(event.target.checked);
            }}
          />
          Scan all
        </label>
        <label>
          <span>Manual limit</span>
          <select
            value={timeoutMs}
            onChange={(event) => setTimeoutMs(Number(event.target.value))}
          >
            <option value={250}>250 ms</option>
            <option value={1000}>1 s</option>
            <option value={2000}>2 s</option>
            <option value={5000}>5 s</option>
            <option value={10000}>10 s</option>
          </select>
        </label>
        <button
          type="button"
          className="primary-button run-button"
          onClick={() => void run("manual")}
          disabled={
            runState.status === "running" ||
            testsRunning ||
            mode === "tests" ||
            mode === "corpus" ||
            !syntax?.accepted ||
            (mode === "replace" && replacementSyntax?.accepted !== true)
          }
        >
          {runState.status === "running" ? "Running…" : "Run"}
        </button>
        {runState.status === "running" ? (
          <button
            type="button"
            className="secondary-button"
            onClick={() => {
              invalidateExecution();
              setRunState({
                status: "idle",
                message: "Execution cancelled; the worker was terminated.",
              });
            }}
          >
            Cancel
          </button>
        ) : null}
      </section>
      {!liveWithinLimits && live ? (
        <p className="limit-banner">
          Live execution is paused above the soft input limit. Syntax parsing
          continues; use Run manually.
        </p>
      ) : null}
      {scanAll && !flags.some((flag) => flag === "g" || flag === "y") ? (
        <p className="scan-banner">
          Scan all is explicit: the worker adds an internal global-iteration
          flag only for this request. The pattern’s saved user flags remain
          unchanged.
        </p>
      ) : null}
      <div className="workspace-toolbar">
        <nav className="mode-tabs" aria-label="Workspace mode">
          {(
            [
              ["match", "Match"],
              ["replace", "Replace"],
              ["list", "List"],
              ["corpus", "Corpus / Apply"],
              ["tests", "Unit tests"],
            ] as const
          ).map(([value, label]) => (
            <button
              key={value}
              type="button"
              aria-current={mode === value ? "page" : undefined}
              disabled={testsRunning}
              onClick={() => {
                if (mode === value) return;
                invalidateExecution(value === "tests");
                setMode(value);
              }}
            >
              {label}
            </button>
          ))}
        </nav>
        <div className="workspace-actions">
          <button
            type="button"
            className="secondary-button"
            onClick={() => setShowGeneration(true)}
            aria-expanded={showGeneration}
            aria-controls="generation-dialog"
          >
            Generate cases
          </button>
          <button
            type="button"
            className="secondary-button"
            onClick={() => setShowFormatter(true)}
            aria-expanded={showFormatter}
            aria-controls="formatter-dialog"
            title={
              flavour === "ecmascript"
                ? "Preview canonical literal/control escaping and validate it against exact snapshots"
                : "Open the formatter to see current flavour availability"
            }
          >
            Format
          </button>
          <button
            type="button"
            className="secondary-button"
            onClick={() => setShowMinimizer(true)}
            aria-expanded={showMinimizer}
            aria-controls="minimize-dialog"
          >
            Minimize
          </button>
          <button
            type="button"
            className="secondary-button"
            onClick={() => setShowComparison(true)}
            aria-expanded={showComparison}
            aria-controls="comparison-code-dialog"
          >
            Compare &amp; code
          </button>
          <button
            type="button"
            className="secondary-button"
            onClick={() => setShowAnalysis(true)}
            aria-expanded={showAnalysis}
            aria-controls="analysis-dialog"
          >
            Analysis
          </button>
          <button
            type="button"
            className="secondary-button"
            disabled={flavour !== "pcre2"}
            title={
              flavour === "pcre2"
                ? "Open the isolated PCRE2 automatic-callout viewer"
                : "Select PCRE2 to enable its automatic-callout trace"
            }
            onClick={() => setShowTrace(true)}
            aria-expanded={showTrace}
            aria-controls="trace-dialog"
          >
            PCRE trace
          </button>
          <button
            type="button"
            className="secondary-button"
            onClick={() => setShowReference((value) => !value)}
            aria-expanded={showReference}
            aria-controls="quick-reference-dialog"
          >
            Quick reference
          </button>
          <button
            type="button"
            className="secondary-button"
            onClick={() => setShowCapabilities((value) => !value)}
            aria-expanded={showCapabilities}
            aria-controls="capabilities-dialog"
          >
            Capabilities
          </button>
          <button
            type="button"
            className="secondary-button"
            onClick={() => setShowProject((value) => !value)}
            aria-expanded={showProject}
          >
            Project
          </button>
        </div>
        <ProjectPanel
          project={currentProject}
          open={showProject}
          onClose={() => setShowProject(false)}
          onImport={importProject}
          onSaveLocal={(includeText, includeUnitTestSubjects) =>
            saveProjectLocally(
              currentProject,
              includeText,
              includeUnitTestSubjects,
            )
          }
          onLoadLocal={async () => {
            const loaded = await loadMostRecentProject();
            if (!loaded)
              throw new Error("No local save exists for this project.");
            importProject(loaded);
          }}
        />
      </div>
      <ModalDialog
        id="generation-dialog"
        open={showGeneration}
        labelledBy="generation-heading"
        onClose={() => setShowGeneration(false)}
        className="generation-dialog"
      >
        {showGeneration ? (
          <GenerationPanel
            active={showGeneration}
            flavour={flavour}
            flavourVersion={flavourVersion}
            pattern={pattern}
            flags={flags}
            options={options}
            scanAll={scanAll}
            syntax={syntax}
            onAddToTests={addGeneratedTests}
            onSelectPatternRange={(range) => {
              const node = syntax
                ? findSmallestNode(syntax.root, range)
                : undefined;
              if (node) {
                selectSyntaxNode(node);
              } else {
                setRequestedPatternRange(range);
              }
              setShowGeneration(false);
            }}
            onClose={() => setShowGeneration(false)}
          />
        ) : null}
      </ModalDialog>
      <ModalDialog
        id="formatter-dialog"
        open={showFormatter}
        labelledBy="formatter-heading"
        onClose={() => setShowFormatter(false)}
        className="formatter-dialog"
      >
        {showFormatter ? (
          <PatternFormatterPanel
            flavour={flavour}
            flavourVersion={flavourVersion}
            pattern={pattern}
            flags={flags}
            options={options}
            syntax={syntax}
            subject={subject}
            replacement={replacement}
            scanAll={scanAll}
            timeoutMs={timeoutMs}
            tests={tests}
            onApply={(value) => {
              changePattern(value);
              setShowFormatter(false);
            }}
            onClose={() => setShowFormatter(false)}
          />
        ) : null}
      </ModalDialog>
      <ModalDialog
        id="minimize-dialog"
        open={showMinimizer}
        labelledBy="minimize-heading"
        onClose={() => setShowMinimizer(false)}
        className="minimize-dialog"
      >
        {showMinimizer ? (
          <MinimizePanel
            activeFlavour={flavour}
            activeFlavourVersion={flavourVersion}
            activeFlags={flags}
            activeOptions={options}
            pattern={pattern}
            subject={subject}
            replacement={replacement}
            scanAll={scanAll}
            timeoutMs={timeoutMs}
            onUseSubject={(value) => {
              changeSubject(value);
              setShowMinimizer(false);
            }}
            onClose={() => setShowMinimizer(false)}
          />
        ) : null}
      </ModalDialog>
      <ModalDialog
        id="comparison-code-dialog"
        open={showComparison}
        labelledBy="comparison-code-heading"
        onClose={() => setShowComparison(false)}
        className="comparison-code-dialog"
      >
        {showComparison ? (
          <ComparisonCodePanel
            activeFlavour={flavour}
            activeFlags={flags}
            activeOptions={options}
            pattern={pattern}
            subject={subject}
            replacement={replacement}
            scanAll={scanAll}
            timeoutMs={timeoutMs}
            onClose={() => setShowComparison(false)}
          />
        ) : null}
      </ModalDialog>
      <ModalDialog
        id="quick-reference-dialog"
        open={showReference}
        labelledBy="reference-heading"
        onClose={() => setShowReference(false)}
        className="reference-dialog"
      >
        <QuickReference
          flavour={flavour}
          onUsePattern={(example) => {
            changePattern(example);
            setShowReference(false);
          }}
          onClose={() => setShowReference(false)}
        />
      </ModalDialog>
      <ModalDialog
        id="capabilities-dialog"
        open={showCapabilities}
        labelledBy="capability-heading"
        onClose={() => setShowCapabilities(false)}
        className="capabilities-dialog"
      >
        <CapabilityPanel
          syntax={syntax}
          execution={execution}
          onClose={() => setShowCapabilities(false)}
        />
      </ModalDialog>
      <ModalDialog
        id="trace-dialog"
        open={showTrace}
        labelledBy="trace-heading"
        onClose={() => setShowTrace(false)}
        className="trace-dialog"
      >
        <Pcre2TracePanel
          active={showTrace}
          flavour={flavour}
          pattern={pattern}
          flags={flags}
          options={options}
          subject={subject}
          timeoutMs={timeoutMs}
          onClose={() => setShowTrace(false)}
          onSelectRanges={(patternRange, subjectRange) => {
            setSelectedSyntax(undefined);
            setSelectedExtraction(undefined);
            setSelectedCaptureRow(undefined);
            setRequestedPatternRange(patternRange);
            setRequestedSubjectRange(subjectRange);
            setShowTrace(false);
          }}
        />
      </ModalDialog>
      <ModalDialog
        id="analysis-dialog"
        open={showAnalysis}
        labelledBy="analysis-heading"
        onClose={() => setShowAnalysis(false)}
        className="analysis-dialog"
      >
        <AnalysisPanel
          active={showAnalysis}
          flavour={flavour}
          pattern={pattern}
          flags={flags}
          subject={subject}
          replacement={replacement}
          scanAll={scanAll}
          syntax={syntax}
          onClose={() => setShowAnalysis(false)}
          onSelectPatternRange={(range) => {
            const node = syntax
              ? findSmallestNode(syntax.root, range)
              : undefined;
            if (node) {
              selectSyntaxNode(node);
            } else {
              setRequestedPatternRange(range);
            }
            setShowAnalysis(false);
          }}
        />
      </ModalDialog>
      <div
        className={`run-status status-${runState.status}`}
        role="status"
        aria-live="polite"
      >
        <span>{runState.status}</span>
        <strong>{runState.message}</strong>
        {execution?.accepted ? (
          <small>
            {execution.matches.length.toLocaleString()} matches ·{" "}
            {execution.elapsedMs.toFixed(2)} ms · native offsets{" "}
            {execution.engine.offsetUnit}
          </small>
        ) : null}
      </div>
      <div className="primary-workspace">
        <section className="panel editor-panel pattern-panel">
          <header className="panel-heading">
            <div>
              <p className="eyebrow">
                Raw pattern · delimiters are presentation only
              </p>
              <h2>Pattern</h2>
            </div>
            <code>/{flags.join("")}</code>
          </header>
          {patternMarkSummary.truncated ? (
            <p
              className="render-limit-banner"
              role="status"
              data-testid="pattern-mark-render-limit"
            >
              <strong>Editor decorations limited.</strong> Showing the first{" "}
              {patternMarkSummary.renderedTokenMarks.toLocaleString()} of{" "}
              {patternMarkSummary.totalTokenMarks.toLocaleString()} syntax
              decorations. The complete normalized syntax model remains
              available.
            </p>
          ) : null}
          <CodeEditor
            value={pattern}
            onChange={changePattern}
            onSelectionChange={(range) => {
              setRequestedPatternRange(undefined);
              const node = syntax?.root
                ? findSmallestNode(syntax.root, range)
                : undefined;
              if (node) {
                selectSyntaxNode(node, false);
              } else {
                setSelectedSyntax(undefined);
                setSelectedExtraction(undefined);
                setSelectedCaptureRow(undefined);
                setRequestedSubjectRange(undefined);
              }
            }}
            label="Regular expression pattern"
            className="pattern-editor"
            maximumLengthUtf16={DEFAULT_REGEX_LIMITS.patternHardLengthUtf16}
            marks={patternMarkSummary.marks}
            selectedRange={requestedPatternRange}
          />
        </section>
        <ExplanationTree
          root={syntax?.root}
          selectedId={selectedSyntax?.id}
          flags={flags}
          onSelect={(node) => selectSyntaxNode(node)}
        />
      </div>
      {mode !== "tests" && mode !== "corpus" ? (
        <div className="result-workspace">
          <section className="panel editor-panel subject-panel">
            <header className="panel-heading">
              <div>
                <p className="eyebrow">Not persisted by default</p>
                <h2>Test text</h2>
              </div>
              <div className="editor-heading-actions">
                <span>{subjectBytes.toLocaleString()} UTF-8 bytes</span>
                <span aria-live="polite">
                  Ln {subjectCursor.line}, Col {subjectCursor.column}
                </span>
                <label className="check-control">
                  <input
                    type="checkbox"
                    checked={showSubjectWhitespace}
                    onChange={(event) =>
                      setShowSubjectWhitespace(event.target.checked)
                    }
                  />
                  Visible whitespace
                </label>
              </div>
            </header>
            {subjectMarkSummary.truncated ? (
              <p
                className="render-limit-banner"
                role="status"
                data-testid="subject-mark-render-limit"
              >
                <strong>Editor highlights limited.</strong> Showing the first{" "}
                {subjectMarkSummary.renderedResultMarks.toLocaleString()} of{" "}
                {subjectMarkSummary.totalResultMarks.toLocaleString()} returned
                match and capture highlights. All returned matches remain
                available in the result views and exports.
              </p>
            ) : null}
            <CodeEditor
              value={subject}
              onChange={changeSubject}
              onSelectionChange={(range) => {
                setRequestedSubjectRange(undefined);
                const node = findSmallestExtractionNode(extraction, range);
                if (node) {
                  selectExtraction(node, false);
                } else {
                  setSelectedExtraction(undefined);
                  setSelectedCaptureRow(undefined);
                }
              }}
              onCursorPositionChange={setSubjectCursor}
              label="Test text"
              className="subject-editor"
              maximumLengthUtf16={
                DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes
              }
              maximumUtf8Bytes={
                DEFAULT_REGEX_LIMITS.interactiveSubjectHardBytes
              }
              lineNumberGutter
              showWhitespace={showSubjectWhitespace}
              marks={subjectMarkSummary.marks}
              selectedRange={requestedSubjectRange}
            />
          </section>
          <ExtractionTree
            nodes={extraction}
            selectedId={selectedExtraction?.id}
            onSelect={(node) => selectExtraction(node)}
            repeatedCaptureNumbers={repeatedCaptures}
          />
        </div>
      ) : null}
      {mode === "match" ? (
        <CaptureTable
          rows={captureRows}
          subject={subject}
          sourceResultTruncated={execution?.truncated ?? false}
          selectedId={selectedCaptureRow?.id}
          onSelect={(row) => {
            const extractionNode = findExtractionNodeById(
              extraction,
              `match-${row.matchNumber}-capture-${row.groupNumber}`,
            );
            if (extractionNode) {
              selectExtraction(extractionNode);
            } else {
              setSelectedCaptureRow(row);
              setSelectedExtraction(undefined);
              setRequestedSubjectRange(
                row.start === undefined
                  ? undefined
                  : {
                      startUtf16: row.start,
                      endUtf16: row.end ?? row.start,
                    },
              );
            }
            const definition = syntax?.captures.find(
              (capture) => capture.number === row.groupNumber,
            );
            if (definition && syntax) {
              const selected = findSmallestNode(syntax.root, definition.range);
              setSelectedSyntax(selected);
              setRequestedPatternRange(
                selected
                  ? {
                      startUtf16: selected.range.startUtf16,
                      endUtf16: selected.range.endUtf16,
                    }
                  : undefined,
              );
            }
          }}
        />
      ) : null}
      {mode === "replace" ? (
        <ReplacementPanel
          flavour={flavour}
          replacement={replacement}
          onReplacementChange={changeReplacement}
          syntax={replacementSyntax}
          result={replacementResult}
          preview={replacementPreview}
          selectedToken={selectedReplacementToken}
          selectedContributionId={selectedReplacementContribution?.id}
          selectedOutputRange={requestedOutputRange}
          onSelectToken={(token) => {
            setSelectedReplacementToken(token);
            setSelectedReplacementContribution(undefined);
            setRequestedOutputRange(undefined);
          }}
          onSelectContribution={selectReplacementContribution}
        />
      ) : null}
      {mode === "list" ? (
        <ListPanel
          templateSource={listTemplate}
          onTemplateChange={setListTemplate}
          matches={execution?.matches ?? []}
          subject={subject}
          sourceResultTruncated={execution?.truncated ?? false}
        />
      ) : null}
      <div hidden={mode !== "corpus"}>
        <CorpusPanel
          key={corpusSession}
          active={mode === "corpus"}
          flavour={flavour}
          flavourVersion={flavourVersion}
          options={options}
          pattern={pattern}
          flags={flags}
          captureMetadata={syntax?.captures ?? []}
          patternAccepted={syntax?.accepted === true}
          replacement={replacement}
          replacementAccepted={replacementSyntax?.accepted === true}
          onReplacementChange={changeReplacement}
          timeoutMs={timeoutMs}
        />
      </div>
      {mode === "tests" ? (
        <UnitTestPanel
          tests={tests}
          results={testResults}
          currentFlavour={flavour}
          currentPattern={pattern}
          currentFlags={flags}
          currentScanAll={scanAll}
          currentSubject={subject}
          currentReplacement={replacement}
          currentResultDraft={currentResultDraft}
          running={testsRunning}
          onAdd={addTest}
          onUpdate={updateTest}
          onClone={cloneTest}
          onImport={importTests}
          onRemove={(id) => {
            setTests((current) => current.filter((test) => test.id !== id));
            discardTestResult(id);
          }}
          onToggle={(id) =>
            setTests((current) =>
              current.map((test) =>
                test.id === id ? { ...test, enabled: !test.enabled } : test,
              ),
            )
          }
          onRunAll={() => void runTests()}
          onRunSelected={(ids) => void runTests(ids)}
          onCancel={cancelTestRun}
        />
      ) : null}
      <DiagnosticsPanel
        diagnostics={diagnostics}
        onSelect={(diagnostic) => {
          const diagnosticRange = diagnostic.range;
          if (diagnostic.id.startsWith("replacement-") && diagnosticRange) {
            const token = replacementSyntax?.tokens.find(
              (candidate) =>
                candidate.range.startUtf16 <= diagnosticRange.startUtf16 &&
                candidate.range.endUtf16 >= diagnosticRange.endUtf16,
            );
            setSelectedReplacementToken(token);
            setSelectedReplacementContribution(undefined);
            setRequestedOutputRange(undefined);
            return;
          }
          if (diagnosticRange && syntax?.root) {
            const selected = findSmallestNode(syntax.root, diagnosticRange);
            setSelectedSyntax(selected);
            setRequestedPatternRange(
              selected
                ? {
                    startUtf16: selected.range.startUtf16,
                    endUtf16: selected.range.endUtf16,
                  }
                : undefined,
            );
          }
        }}
      />
    </main>
  );
}
