import {
  DEFAULT_REGEX_LIMITS,
  utf8ByteLength,
} from "../execution/request-limits";
import type { NormalizedRegexNode } from "../model/syntax";
import { GENERATION_LIMITS } from "./generation-limits";
import {
  GENERATED_CASE_GENERATOR_ID,
  GENERATED_CASE_GENERATOR_VERSION,
  type CandidateGenerationReport,
  type CandidateGenerationRequest,
  type GeneratedSubjectCandidate,
  type GenerationCoverageEntry,
  type GenerationCoverageStatus,
  type GenerationFeature,
  type UnsupportedGenerationConstruct,
} from "./generation.types";
import {
  createDeterministicRandom,
  generationSeedHash,
  type DeterministicRandom,
} from "./seeded-random";

const MAXIMUM_WARNINGS = 100;
const MAXIMUM_UNSUPPORTED_CONSTRUCTS = 250;
const MAXIMUM_RAW_PREVIEW_UTF16 = 80;

interface AstRecord {
  readonly node: NormalizedRegexNode;
  readonly depth: number;
  readonly parent?: NormalizedRegexNode;
}

interface Fragment {
  readonly text: string;
  readonly bytes: number;
}

interface RenderOverrides {
  readonly choices?: ReadonlyMap<string, number>;
  readonly repetitions?: ReadonlyMap<string, number>;
  readonly classChoices?: ReadonlyMap<string, number>;
  readonly random?: DeterministicRandom;
}

class RenderLimitError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "RenderLimitError";
  }
}

function boundedInteger(
  value: number,
  label: string,
  minimum: number,
  maximum: number,
): void {
  if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
    throw new RangeError(
      `${label} must be a whole number from ${minimum.toLocaleString()} to ${maximum.toLocaleString()}.`,
    );
  }
}

export function validateGenerationRequest(
  request: CandidateGenerationRequest,
): void {
  if (request.root.raw.length > DEFAULT_REGEX_LIMITS.patternHardLengthUtf16) {
    throw new RangeError(
      `Normalized pattern exceeds the ${DEFAULT_REGEX_LIMITS.patternHardLengthUtf16.toLocaleString()} UTF-16 unit limit.`,
    );
  }
  if (
    request.seed.length === 0 ||
    request.seed.length > GENERATION_LIMITS.maximumSeedUtf16
  ) {
    throw new RangeError(
      `Generation seed must contain 1 to ${GENERATION_LIMITS.maximumSeedUtf16} UTF-16 units.`,
    );
  }
  const { settings } = request;
  boundedInteger(
    settings.maximumCases,
    "Maximum generated cases",
    1,
    DEFAULT_REGEX_LIMITS.maximumGeneratedCases,
  );
  boundedInteger(
    settings.maximumCandidateAttempts,
    "Maximum candidate attempts",
    settings.maximumCases,
    GENERATION_LIMITS.maximumCandidateAttempts,
  );
  boundedInteger(
    settings.maximumTotalSubjectBytes,
    "Maximum generated subject bytes",
    1,
    GENERATION_LIMITS.maximumTotalSubjectBytes,
  );
  boundedInteger(
    settings.maximumSubjectBytes,
    "Maximum bytes per generated subject",
    1,
    GENERATION_LIMITS.maximumSubjectBytes,
  );
  if (settings.maximumSubjectBytes > settings.maximumTotalSubjectBytes) {
    throw new RangeError(
      "The per-subject byte limit cannot exceed the aggregate generated-subject limit.",
    );
  }
  boundedInteger(
    settings.maximumAstNodes,
    "Maximum AST nodes",
    1,
    GENERATION_LIMITS.maximumAstNodes,
  );
  boundedInteger(
    settings.maximumAstDepth,
    "Maximum AST depth",
    1,
    GENERATION_LIMITS.maximumAstDepth,
  );
  boundedInteger(
    settings.maximumQuantifierRepetitions,
    "Maximum synthesized repetitions",
    1,
    GENERATION_LIMITS.maximumQuantifierRepetitions,
  );
  boundedInteger(
    settings.randomVariantCount,
    "Random variants",
    0,
    DEFAULT_REGEX_LIMITS.maximumGeneratedCases,
  );
  boundedInteger(
    settings.perCaseTimeoutMs,
    "Per-case timeout",
    GENERATION_LIMITS.minimumPerCaseTimeoutMs,
    GENERATION_LIMITS.maximumPerCaseTimeoutMs,
  );
  boundedInteger(
    settings.maximumWallTimeMs,
    "Generation wall time",
    1,
    GENERATION_LIMITS.maximumWallTimeMs,
  );
  if (request.root.support.flavour !== request.flavour) {
    throw new Error(
      `The normalized AST belongs to ${request.root.support.flavour}, not ${request.flavour}.`,
    );
  }
}

function preview(value: string, maximum = MAXIMUM_RAW_PREVIEW_UTF16): string {
  return value.length <= maximum ? value : `${value.slice(0, maximum)}…`;
}

function recordsFor(
  root: NormalizedRegexNode,
  maximumNodes: number,
  maximumDepth: number,
): {
  readonly records: readonly AstRecord[];
  readonly traversalTruncated: boolean;
  readonly depthTruncated: boolean;
} {
  const records: AstRecord[] = [];
  const stack: AstRecord[] = [{ node: root, depth: 1 }];
  let traversalTruncated = false;
  let depthTruncated = false;
  while (stack.length > 0) {
    const record = stack.pop() as AstRecord;
    if (record.depth > maximumDepth) {
      depthTruncated = true;
      continue;
    }
    if (records.length >= maximumNodes) {
      traversalTruncated = true;
      break;
    }
    records.push(record);
    for (let index = record.node.children.length - 1; index >= 0; index -= 1) {
      stack.push({
        node: record.node.children[index] as NormalizedRegexNode,
        depth: record.depth + 1,
        parent: record.node,
      });
    }
  }
  return { records, traversalTruncated, depthTruncated };
}

function decodeEscapedAtom(raw: string): string | undefined {
  if (!raw.startsWith("\\")) return raw;
  const simple: Readonly<Record<string, string>> = {
    "\\0": "\0",
    "\\b": "\b",
    "\\f": "\f",
    "\\n": "\n",
    "\\r": "\r",
    "\\t": "\t",
    "\\v": "\v",
  };
  if (raw in simple) return simple[raw];
  const hex = /^\\x([\da-f]{2})$/iu.exec(raw);
  if (hex?.[1]) return String.fromCharCode(Number.parseInt(hex[1], 16));
  const unicode = /^\\u([\da-f]{4})$/iu.exec(raw);
  if (unicode?.[1]) {
    return String.fromCharCode(Number.parseInt(unicode[1], 16));
  }
  const unicodePoint = /^\\u\{([\da-f]{1,6})\}$/iu.exec(raw);
  if (unicodePoint?.[1]) {
    const value = Number.parseInt(unicodePoint[1], 16);
    if (value <= 0x10ffff) return String.fromCodePoint(value);
    return undefined;
  }
  const control = /^\\c([a-z])$/iu.exec(raw);
  if (control?.[1]) {
    return String.fromCharCode(control[1].toUpperCase().charCodeAt(0) % 32);
  }
  if (/^\\[^dDsSwWpPkK]$/u.test(raw)) return raw.slice(1);
  return undefined;
}

function unique(values: readonly string[]): readonly string[] {
  return [...new Set(values)];
}

function unicodePropertyRepresentatives(raw: string): readonly string[] {
  const lower = raw.toLocaleLowerCase("en-US");
  const negated = raw.startsWith("\\P");
  if (negated) {
    if (/letter|\\p\{l\}/iu.test(raw)) return ["0", "_", " "];
    if (/number|decimal_number|\\p\{n[d]?\}/iu.test(raw)) {
      return ["A", "_", " "];
    }
    if (/white_space|pattern_white_space/iu.test(raw)) return ["A", "0", "_"];
    return ["A", "0", " ", "😀"];
  }
  if (lower.includes("letter") || /^\\p\{l[ultmo]?\}$/iu.test(raw)) {
    return ["A", "é", "Ж", "Ω"];
  }
  if (
    lower.includes("number") ||
    lower.includes("decimal_number") ||
    /^\\p\{n[dlo]?\}$/iu.test(raw)
  ) {
    return ["0", "٥", "Ⅷ"];
  }
  if (lower.includes("greek")) return ["Ω", "β"];
  if (lower.includes("cyrillic")) return ["Ж", "я"];
  if (lower.includes("latin")) return ["A", "é"];
  if (lower.includes("emoji")) return ["😀", "©"];
  if (lower.includes("white_space")) return [" ", "\t", "\n"];
  if (lower.includes("punctuation") || /^\\p\{p\}$/iu.test(raw)) {
    return [".", "—", "!"];
  }
  if (lower.includes("symbol") || /^\\p\{s\}$/iu.test(raw)) {
    return ["+", "€", "😀"];
  }
  return ["A", "0", "é", "Ω", "Ж", "😀", " "];
}

function unicodePropertyCoverageGap(raw: string): string | undefined {
  const lower = raw.toLocaleLowerCase("en-US");
  if (
    lower.includes("rgi_emoji") ||
    lower.includes("basic_emoji") ||
    lower.includes("emoji_keycap_sequence") ||
    lower.includes("emoji_flag_sequence") ||
    lower.includes("emoji_tag_sequence") ||
    lower.includes("emoji_zwj_sequence")
  ) {
    return "Unicode properties of strings can consume multiple code points and are not structurally enumerated.";
  }
  if (
    lower.includes("letter") ||
    /^\\[pP]\{l[ultmo]?\}$/u.test(raw) ||
    lower.includes("number") ||
    lower.includes("decimal_number") ||
    /^\\[pP]\{n[dlo]?\}$/u.test(raw) ||
    lower.includes("greek") ||
    lower.includes("cyrillic") ||
    lower.includes("latin") ||
    lower.includes("emoji") ||
    lower.includes("white_space") ||
    lower.includes("punctuation") ||
    /^\\[pP]\{p\}$/u.test(raw) ||
    lower.includes("symbol") ||
    /^\\[pP]\{s\}$/u.test(raw)
  ) {
    return undefined;
  }
  return "This Unicode property uses only the generic bounded representative pool; its value space is not claimed as covered.";
}

function leafClassRepresentatives(raw: string): readonly string[] {
  switch (raw) {
    case "\\d":
      return ["0", "5", "9"];
    case "\\D":
      return ["A", "_", "é", " "];
    case "\\w":
      return ["A", "0", "_"];
    case "\\W":
      return ["-", " ", "é"];
    case "\\s":
      return [" ", "\t", "\n", "\u00a0"];
    case "\\S":
      return ["A", "0", "_", "é"];
    default:
      return raw.startsWith("\\p") || raw.startsWith("\\P")
        ? unicodePropertyRepresentatives(raw)
        : [];
  }
}

function descendants(
  node: NormalizedRegexNode,
  maximum = 256,
): readonly NormalizedRegexNode[] {
  const output: NormalizedRegexNode[] = [];
  const stack = [...node.children].reverse();
  while (stack.length > 0 && output.length < maximum) {
    const current = stack.pop() as NormalizedRegexNode;
    output.push(current);
    for (let index = current.children.length - 1; index >= 0; index -= 1) {
      stack.push(current.children[index] as NormalizedRegexNode);
    }
  }
  return output;
}

function classRepresentatives(node: NormalizedRegexNode): readonly string[] {
  if (node.kind === "dot") return ["a", "0", "é", "😀"];
  if (node.kind === "unicode-property") {
    return unicodePropertyRepresentatives(node.raw);
  }
  const leaf = leafClassRepresentatives(node.raw);
  if (leaf.length > 0) return leaf;
  if (node.kind === "character-class-range") {
    const endpoints = node.children
      .map((child) => decodeEscapedAtom(child.raw))
      .filter((value): value is string => value !== undefined);
    if (endpoints.length >= 2) {
      const start = endpoints[0]?.codePointAt(0);
      const end = endpoints[1]?.codePointAt(0);
      const middle =
        start !== undefined && end !== undefined && end >= start
          ? String.fromCodePoint(start + Math.floor((end - start) / 2))
          : undefined;
      return unique([
        endpoints[0] as string,
        ...(middle ? [middle] : []),
        endpoints[1] as string,
      ]);
    }
  }
  if (!node.raw.startsWith("[")) {
    const literal = decodeEscapedAtom(node.raw);
    return literal === undefined ? [] : [literal];
  }
  if (node.raw.startsWith("[^")) {
    return ["A", "0", "_", " ", "-", "é", "Ω", "Ж", "😀", "\n"];
  }
  const values: string[] = [];
  for (const child of descendants(node)) {
    if (child.kind === "literal" || child.kind === "escaped-literal") {
      const decoded = decodeEscapedAtom(child.raw);
      if (decoded !== undefined) values.push(decoded);
    } else if (
      child.kind === "character-class-range" ||
      child.kind === "unicode-property"
    ) {
      values.push(...classRepresentatives(child));
    } else if (child.kind === "character-class" && !child.raw.startsWith("[")) {
      values.push(...leafClassRepresentatives(child.raw));
    }
  }
  return unique(values);
}

function choiceChildren(
  node: NormalizedRegexNode,
): readonly NormalizedRegexNode[] {
  switch (node.kind) {
    case "pattern":
    case "disjunction":
    case "capture-group":
    case "named-capture-group":
    case "noncapture-group":
    case "inline-flags":
      return node.children;
    default:
      return [];
  }
}

function shortestChoice(children: readonly NormalizedRegexNode[]): number {
  let selected = 0;
  let length = Number.POSITIVE_INFINITY;
  children.forEach((child, index) => {
    const candidate =
      child.properties.minimumLength ?? Number.POSITIVE_INFINITY;
    if (candidate < length) {
      selected = index;
      length = candidate;
    }
  });
  return selected;
}

function appendFragments(
  fragments: readonly Fragment[],
  maximumBytes: number,
): Fragment {
  let bytes = 0;
  for (const fragment of fragments) {
    bytes += fragment.bytes;
    if (bytes > maximumBytes) {
      throw new RenderLimitError(
        `Candidate exceeds the ${maximumBytes.toLocaleString()} byte per-subject limit.`,
      );
    }
  }
  return { text: fragments.map((fragment) => fragment.text).join(""), bytes };
}

function repeatFragment(
  fragment: Fragment,
  repetitions: number,
  maximumBytes: number,
): Fragment {
  if (
    fragment.bytes > 0 &&
    repetitions > Math.floor(maximumBytes / fragment.bytes)
  ) {
    throw new RenderLimitError(
      `Quantifier expansion exceeds the ${maximumBytes.toLocaleString()} byte per-subject limit.`,
    );
  }
  const bytes = fragment.bytes * repetitions;
  if (bytes > maximumBytes) {
    throw new RenderLimitError(
      `Quantifier expansion exceeds the ${maximumBytes.toLocaleString()} byte per-subject limit.`,
    );
  }
  return { text: fragment.text.repeat(repetitions), bytes };
}

function renderNode(
  node: NormalizedRegexNode,
  overrides: RenderOverrides,
  maximumBytes: number,
  maximumDepth: number,
  maximumRepetitions: number,
  depth = 1,
): Fragment {
  if (depth > maximumDepth) {
    throw new RenderLimitError(
      `Candidate rendering exceeds the ${maximumDepth.toLocaleString()} level AST-depth limit.`,
    );
  }
  switch (node.kind) {
    case "literal":
    case "escaped-literal": {
      const decoded = decodeEscapedAtom(node.raw);
      if (decoded === undefined) return { text: "", bytes: 0 };
      return { text: decoded, bytes: utf8ByteLength(decoded) };
    }
    case "dot": {
      const representatives = classRepresentatives(node);
      const selected =
        overrides.classChoices?.get(node.id) ??
        (overrides.random
          ? overrides.random.integer(representatives.length)
          : 0);
      const text = representatives[selected % representatives.length] ?? "a";
      return { text, bytes: utf8ByteLength(text) };
    }
    case "character-class":
    case "character-class-range":
    case "unicode-property": {
      const representatives = classRepresentatives(node);
      if (representatives.length === 0) return { text: "", bytes: 0 };
      const selected =
        overrides.classChoices?.get(node.id) ??
        (overrides.random
          ? overrides.random.integer(representatives.length)
          : 0);
      const text =
        representatives[selected % representatives.length] ??
        (representatives[0] as string);
      return { text, bytes: utf8ByteLength(text) };
    }
    case "quantifier": {
      const child = node.children[0];
      if (!child) return { text: "", bytes: 0 };
      const configured = overrides.repetitions?.get(node.id);
      const minimum = node.quantifier?.minimum ?? 0;
      const maximum = node.quantifier?.maximum;
      let repetitions =
        configured ??
        (overrides.random
          ? minimum +
            overrides.random.integer(
              Math.max(
                1,
                Math.min(
                  4,
                  maximum === null || maximum === undefined
                    ? 4
                    : maximum - minimum + 1,
                ),
              ),
            )
          : minimum);
      if (maximum !== null && maximum !== undefined) {
        repetitions = Math.min(repetitions, maximum);
      }
      if (repetitions > maximumRepetitions) {
        throw new RenderLimitError(
          `Quantifier expansion exceeds the ${maximumRepetitions.toLocaleString()} repetition synthesis limit.`,
        );
      }
      const fragment = renderNode(
        child,
        overrides,
        maximumBytes,
        maximumDepth,
        maximumRepetitions,
        depth + 1,
      );
      return repeatFragment(fragment, repetitions, maximumBytes);
    }
    case "anchor":
    case "word-boundary":
    case "lookahead":
    case "negative-lookahead":
    case "lookbehind":
    case "negative-lookbehind":
    case "backreference":
    case "subroutine-call":
    case "recursion":
    case "conditional":
    case "comment":
    case "control-verb":
    case "callout":
    case "unsupported":
    case "error-recovery":
      return { text: "", bytes: 0 };
    default: {
      const choices = choiceChildren(node);
      if (choices.length > 0) {
        const configured = overrides.choices?.get(node.id);
        const selected =
          configured ??
          (overrides.random
            ? overrides.random.integer(choices.length)
            : shortestChoice(choices));
        return renderNode(
          choices[selected % choices.length] as NormalizedRegexNode,
          overrides,
          maximumBytes,
          maximumDepth,
          maximumRepetitions,
          depth + 1,
        );
      }
      return appendFragments(
        node.children.map((child) =>
          renderNode(
            child,
            overrides,
            maximumBytes,
            maximumDepth,
            maximumRepetitions,
            depth + 1,
          ),
        ),
        maximumBytes,
      );
    }
  }
}

function quantifierCounts(
  node: NormalizedRegexNode,
  maximumRepetitions: number,
): {
  readonly counts: readonly number[];
  readonly truncated: boolean;
} {
  const minimum = node.quantifier?.minimum ?? 0;
  const maximum = node.quantifier?.maximum;
  const candidates = new Set<number>([minimum]);
  if (minimum < maximumRepetitions) candidates.add(minimum + 1);
  if (minimum === 0 && maximumRepetitions >= 2) candidates.add(2);
  let truncated = minimum > maximumRepetitions;
  if (maximum === null || maximum === undefined) {
    candidates.add(Math.min(maximumRepetitions, Math.max(minimum, 3)));
  } else {
    const boundedMaximum = Math.min(maximum, maximumRepetitions);
    candidates.add(boundedMaximum);
    if (boundedMaximum > minimum) candidates.add(boundedMaximum - 1);
    if (maximum > maximumRepetitions) truncated = true;
  }
  return {
    counts: [...candidates]
      .filter((count) => count >= 0 && count <= maximumRepetitions)
      .sort((left, right) => left - right),
    truncated,
  };
}

function unsupportedReason(node: NormalizedRegexNode): string | undefined {
  switch (node.kind) {
    case "backreference":
      return "Backreference text depends on capture participation and is not synthesized.";
    case "subroutine-call":
      return "Subroutine calls are not expanded by the generator.";
    case "recursion":
      return "Recursive patterns are not expanded by the generator.";
    case "conditional":
      return "Conditional branches depend on runtime capture or assertion state.";
    case "branch-reset-group":
      return "Branch-reset numbering needs a complete flavour-specific structural provider.";
    case "atomic-group":
      return "Atomic grouping is not structurally generated in version 1.";
    case "control-verb":
      return "Engine control verbs are not interpreted by the generator.";
    case "callout":
      return "Engine callouts are not interpreted by the generator.";
    case "lookahead":
    case "negative-lookahead":
    case "lookbehind":
    case "negative-lookbehind":
      return "Lookaround conditions are not synthesized; engine verification may still accept surrounding candidates.";
    case "word-boundary":
      return "Word-boundary context is sampled but is not solved symbolically.";
    case "error-recovery":
      return "Recovered syntax is not valid input for generation.";
    case "unsupported":
      return "The active syntax provider marked this construct unsupported.";
    case "character-class":
      if (
        node.raw.startsWith("[") &&
        (node.raw.includes("&&") ||
          node.raw.includes("--") ||
          node.raw.includes("\\q{"))
      ) {
        return "Complex Unicode-set algebra and string properties are not synthesized.";
      }
      return classRepresentatives(node).length === 0
        ? "No bounded representative is available for this character class."
        : undefined;
    case "unicode-property":
      return unicodePropertyCoverageGap(node.raw);
    case "escaped-literal":
      return decodeEscapedAtom(node.raw) === undefined
        ? "This escaped atom is not decoded by the generator."
        : undefined;
    default:
      return undefined;
  }
}

function coverageEntry(
  feature: GenerationFeature,
  status: GenerationCoverageStatus,
  detail: string,
  relevantNodes: number,
): GenerationCoverageEntry {
  return { feature, status, detail, relevantNodes };
}

function nearMisses(subject: string): readonly string[] {
  if (subject.length === 0) return ["x", "\n", " "];
  const middle = Math.floor(subject.length / 2);
  const replacement = subject[middle] === "!" ? "x" : "!";
  return unique([
    "",
    subject.slice(1),
    subject.slice(0, -1),
    `${subject.slice(0, middle)}${replacement}${subject.slice(middle + 1)}`,
    `x${subject}`,
    `${subject}x`,
  ]);
}

export function generateCandidateSubjects(
  request: CandidateGenerationRequest,
): CandidateGenerationReport {
  validateGenerationRequest(request);
  const { records, traversalTruncated, depthTruncated } = recordsFor(
    request.root,
    request.settings.maximumAstNodes,
    request.settings.maximumAstDepth,
  );
  const seedHash = generationSeedHash(request.seed);
  const warnings: string[] = [];
  const warn = (message: string) => {
    if (warnings.length < MAXIMUM_WARNINGS && !warnings.includes(message)) {
      warnings.push(message);
    }
  };

  const featureNodes = {
    alternatives: records.filter(({ node }) => choiceChildren(node).length > 1),
    quantifiers: records.filter(({ node }) => node.kind === "quantifier"),
    optionals: records.filter(
      ({ node }) =>
        node.kind === "quantifier" && node.quantifier?.minimum === 0,
    ),
    classes: records.filter(
      ({ node, parent }) =>
        ["dot", "character-class", "unicode-property"].includes(node.kind) &&
        !parent?.kind.startsWith("character-class"),
    ),
    unicode: records.filter(
      ({ node, parent }) =>
        !parent?.kind.startsWith("character-class") &&
        (node.kind === "unicode-property" ||
          node.raw.includes("\\p") ||
          node.raw.includes("\\P")),
    ),
    anchors: records.filter(
      ({ node }) => node.kind === "anchor" || node.kind === "word-boundary",
    ),
  };

  if (request.flavour !== "ecmascript") {
    const detail =
      "Version 1 generation requires the complete ECMAScript normalized AST. The selected flavour’s partial provider is not reinterpreted as ECMAScript.";
    return {
      generator: {
        id: GENERATED_CASE_GENERATOR_ID,
        version: GENERATED_CASE_GENERATOR_VERSION,
      },
      flavour: request.flavour,
      seed: request.seed,
      seedHash,
      candidates: [],
      coverage: (
        [
          "shortest",
          "alternative-coverage",
          "quantifier-boundary",
          "optional-presence",
          "character-class",
          "unicode-representative",
          "likely-near-miss",
          "anchor-boundary",
          "seeded-random",
        ] as const
      ).map((feature) => coverageEntry(feature, "unsupported", detail, 0)),
      unsupportedConstructs: [
        {
          id: "unsupported-flavour",
          kind: request.root.kind,
          range: request.root.range,
          rawPreview: preview(request.root.raw),
          reason: detail,
        },
      ],
      visitedAstNodes: records.length,
      candidateBytes: 0,
      candidateAttempts: 0,
      duplicateCandidates: 0,
      candidatesTruncated: false,
      traversalTruncated: traversalTruncated || depthTruncated,
      stoppedReason: detail,
      warnings: [detail],
    };
  }

  if (traversalTruncated || depthTruncated) {
    const detail = traversalTruncated
      ? `Generation stopped at the ${request.settings.maximumAstNodes.toLocaleString()} node AST traversal limit.`
      : `Generation stopped at the ${request.settings.maximumAstDepth.toLocaleString()} level AST-depth limit.`;
    return {
      generator: {
        id: GENERATED_CASE_GENERATOR_ID,
        version: GENERATED_CASE_GENERATOR_VERSION,
      },
      flavour: request.flavour,
      seed: request.seed,
      seedHash,
      candidates: [],
      coverage: (
        [
          "shortest",
          "alternative-coverage",
          "quantifier-boundary",
          "optional-presence",
          "character-class",
          "unicode-representative",
          "likely-near-miss",
          "anchor-boundary",
          "seeded-random",
        ] as const
      ).map((feature) => coverageEntry(feature, "unsupported", detail, 0)),
      unsupportedConstructs: [],
      visitedAstNodes: records.length,
      candidateBytes: 0,
      candidateAttempts: 0,
      duplicateCandidates: 0,
      candidatesTruncated: false,
      traversalTruncated: true,
      stoppedReason: detail,
      warnings: [detail],
    };
  }

  const unsupportedConstructs: UnsupportedGenerationConstruct[] = [];
  for (const { node } of records) {
    const reason = unsupportedReason(node);
    if (!reason) continue;
    if (unsupportedConstructs.length < MAXIMUM_UNSUPPORTED_CONSTRUCTS) {
      unsupportedConstructs.push({
        id: `unsupported-${unsupportedConstructs.length + 1}`,
        kind: node.kind,
        range: node.range,
        rawPreview: preview(node.raw),
        reason,
      });
    } else {
      warn(
        `Unsupported-construct reporting stopped at ${MAXIMUM_UNSUPPORTED_CONSTRUCTS.toLocaleString()} entries.`,
      );
      break;
    }
  }

  const candidates: GeneratedSubjectCandidate[] = [];
  const candidateKeys = new Set<string>();
  let candidateAttempts = 0;
  let duplicateCandidates = 0;
  let candidateBytes = 0;
  let candidatesTruncated = false;
  let stoppedReason: string | undefined;

  const addCandidate = (
    intendedOutcome: GeneratedSubjectCandidate["intendedOutcome"],
    subject: string,
    label: string,
    features: readonly GenerationFeature[],
    notes: readonly string[] = [],
  ): boolean => {
    if (candidateAttempts >= request.settings.maximumCandidateAttempts) {
      candidatesTruncated = true;
      stoppedReason ??= `Candidate synthesis reached the ${request.settings.maximumCandidateAttempts.toLocaleString()} attempt limit.`;
      return false;
    }
    candidateAttempts += 1;
    const bytes = utf8ByteLength(subject);
    if (bytes > request.settings.maximumSubjectBytes) {
      warn(
        `At least one candidate exceeded the ${request.settings.maximumSubjectBytes.toLocaleString()} byte per-subject limit and was discarded.`,
      );
      return true;
    }
    const key = `${intendedOutcome}\0${subject}`;
    if (candidateKeys.has(key)) {
      duplicateCandidates += 1;
      return true;
    }
    if (candidateBytes + bytes > request.settings.maximumTotalSubjectBytes) {
      candidatesTruncated = true;
      stoppedReason ??= `Candidate synthesis reached the ${request.settings.maximumTotalSubjectBytes.toLocaleString()} byte aggregate subject limit.`;
      return false;
    }
    candidateKeys.add(key);
    candidateBytes += bytes;
    candidates.push({
      id: `generated-${seedHash}-${candidates.length + 1}`,
      intendedOutcome,
      subject,
      subjectBytes: bytes,
      label,
      features: unique(features) as readonly GenerationFeature[],
      notes,
    });
    return true;
  };

  const render = (
    overrides: RenderOverrides,
    label: string,
    features: readonly GenerationFeature[],
    notes?: readonly string[],
  ) => {
    try {
      const fragment = renderNode(
        request.root,
        overrides,
        request.settings.maximumSubjectBytes,
        request.settings.maximumAstDepth,
        request.settings.maximumQuantifierRepetitions,
      );
      addCandidate("match", fragment.text, label, features, notes);
    } catch (error) {
      if (error instanceof RenderLimitError) warn(error.message);
      else throw error;
    }
  };

  render(
    {},
    "Shortest structural candidate",
    ["shortest"],
    unsupportedConstructs.length > 0
      ? [
          "Some pattern constructs are outside generator coverage; actual-engine verification is authoritative.",
        ]
      : [],
  );

  for (const { node } of featureNodes.alternatives) {
    const alternatives = choiceChildren(node);
    for (let index = 0; index < alternatives.length; index += 1) {
      if (candidatesTruncated) break;
      render(
        { choices: new Map([[node.id, index]]) },
        `Alternative ${index + 1} at ${node.range.startUtf16}`,
        ["alternative-coverage"],
      );
    }
  }

  let quantifierCoverageTruncated = false;
  for (const { node } of featureNodes.quantifiers) {
    const counts = quantifierCounts(
      node,
      request.settings.maximumQuantifierRepetitions,
    );
    quantifierCoverageTruncated ||= counts.truncated;
    for (const count of counts.counts) {
      if (candidatesTruncated) break;
      render(
        { repetitions: new Map([[node.id, count]]) },
        `Quantifier boundary ${count} at ${node.range.startUtf16}`,
        [
          "quantifier-boundary",
          ...(node.quantifier?.minimum === 0
            ? (["optional-presence"] as const)
            : []),
        ],
        counts.truncated
          ? [
              `The exact upper boundary exceeds the ${request.settings.maximumQuantifierRepetitions.toLocaleString()} synthesis cap.`,
            ]
          : [],
      );
    }
  }

  for (const { node } of featureNodes.classes) {
    const representatives = classRepresentatives(node);
    representatives.slice(0, 8).forEach((_, index) => {
      if (candidatesTruncated) return;
      render(
        { classChoices: new Map([[node.id, index]]) },
        `Character-class representative ${index + 1} at ${node.range.startUtf16}`,
        [
          "character-class",
          ...(node.kind === "unicode-property" ||
          node.raw.startsWith("\\p") ||
          node.raw.startsWith("\\P")
            ? (["unicode-representative"] as const)
            : []),
        ],
      );
    });
  }
  for (const { node } of featureNodes.unicode) {
    const representatives = classRepresentatives(node);
    representatives.slice(0, 8).forEach((_, index) => {
      if (candidatesTruncated) return;
      render(
        { classChoices: new Map([[node.id, index]]) },
        `Unicode representative ${index + 1} at ${node.range.startUtf16}`,
        ["unicode-representative"],
      );
    });
  }

  const random = createDeterministicRandom(request.seed);
  for (
    let index = 0;
    index < request.settings.randomVariantCount && !candidatesTruncated;
    index += 1
  ) {
    render(
      { random },
      `Seeded variant ${index + 1}`,
      ["seeded-random"],
      [`Seed ${JSON.stringify(request.seed)} · hash ${seedHash}`],
    );
  }

  const positiveSubjects = candidates
    .filter((candidate) => candidate.intendedOutcome === "match")
    .map((candidate) => candidate.subject)
    .slice(0, 32);

  if (featureNodes.anchors.length > 0 && positiveSubjects[0] !== undefined) {
    const subject = positiveSubjects[0];
    const multiline = request.flags.includes("m");
    addCandidate(
      multiline ? "match" : "no-match",
      `x\n${subject}`,
      "Start-anchor boundary",
      ["anchor-boundary"],
      [
        multiline
          ? "Multiline mode makes a post-newline start a likely match boundary."
          : "Without multiline mode, a non-empty prefix is a likely near miss.",
      ],
    );
    addCandidate(
      multiline ? "match" : "no-match",
      `${subject}\nx`,
      "End-anchor boundary",
      ["anchor-boundary"],
      [
        multiline
          ? "Multiline mode makes a pre-newline end a likely match boundary."
          : "Without multiline mode, a non-newline suffix is a likely near miss.",
      ],
    );
  }

  for (const subject of positiveSubjects) {
    if (candidatesTruncated) break;
    for (const candidate of nearMisses(subject)) {
      if (
        !addCandidate(
          "no-match",
          candidate,
          "Likely near miss",
          ["likely-near-miss"],
          [
            "This mutation is retained only if the selected actual engine reports no match.",
          ],
        )
      ) {
        break;
      }
    }
  }

  if (quantifierCoverageTruncated) {
    warn(
      `At least one quantifier boundary exceeded the ${request.settings.maximumQuantifierRepetitions.toLocaleString()} repetition synthesis cap.`,
    );
  }

  const partialFromUnsupported = unsupportedConstructs.length > 0;
  const limited = candidatesTruncated || quantifierCoverageTruncated;
  const applicable = (
    relevant: number,
    completeDetail: string,
    unavailableDetail: string,
  ): {
    readonly status: GenerationCoverageStatus;
    readonly detail: string;
  } =>
    relevant === 0
      ? { status: "not-applicable", detail: unavailableDetail }
      : partialFromUnsupported || limited
        ? {
            status: "partial",
            detail: `${completeDetail} Coverage is bounded and some requested or structural paths were omitted.`,
          }
        : { status: "covered", detail: completeDetail };

  const shortestStatus: GenerationCoverageStatus = partialFromUnsupported
    ? "partial"
    : "covered";
  const alternative = applicable(
    featureNodes.alternatives.length,
    "Each structurally visible alternative was requested at least once.",
    "The normalized AST contains no multi-way alternative.",
  );
  const quantifier = applicable(
    featureNodes.quantifiers.length,
    "Minimum, adjacent and bounded upper repetition samples were requested.",
    "The normalized AST contains no quantifier.",
  );
  const optional = applicable(
    featureNodes.optionals.length,
    "Optional-absent and optional-present boundaries were requested.",
    "The normalized AST contains no optional quantifier.",
  );
  const characterClass = applicable(
    featureNodes.classes.length,
    "Bounded representatives were requested for structurally visible classes.",
    "The normalized AST contains no character class or dot.",
  );
  const unicode = applicable(
    featureNodes.unicode.length,
    "A bounded, documented representative set was requested for visible Unicode properties.",
    "The normalized AST contains no Unicode property.",
  );
  const anchor = applicable(
    featureNodes.anchors.length,
    "Newline and non-boundary context candidates were requested.",
    "The normalized AST contains no anchor or word-boundary assertion.",
  );

  const coverage: GenerationCoverageEntry[] = [
    coverageEntry(
      "shortest",
      shortestStatus,
      partialFromUnsupported
        ? "A shortest structural candidate was requested, but unsupported zero-width or stateful constructs were not solved."
        : "A candidate using shortest alternatives and minimum repetitions was requested.",
      1,
    ),
    coverageEntry(
      "alternative-coverage",
      alternative.status,
      alternative.detail,
      featureNodes.alternatives.length,
    ),
    coverageEntry(
      "quantifier-boundary",
      quantifier.status,
      quantifier.detail,
      featureNodes.quantifiers.length,
    ),
    coverageEntry(
      "optional-presence",
      optional.status,
      optional.detail,
      featureNodes.optionals.length,
    ),
    coverageEntry(
      "character-class",
      characterClass.status,
      characterClass.detail,
      featureNodes.classes.length,
    ),
    coverageEntry(
      "unicode-representative",
      unicode.status,
      unicode.detail,
      featureNodes.unicode.length,
    ),
    coverageEntry(
      "likely-near-miss",
      positiveSubjects.length > 0
        ? candidatesTruncated
          ? "partial"
          : "covered"
        : "unsupported",
      positiveSubjects.length > 0
        ? "Deletion, replacement and boundary mutations were requested; only actual-engine non-matches may be retained."
        : "No positive structural subject was available to mutate.",
      positiveSubjects.length,
    ),
    coverageEntry(
      "anchor-boundary",
      anchor.status,
      anchor.detail,
      featureNodes.anchors.length,
    ),
    coverageEntry(
      "seeded-random",
      request.settings.randomVariantCount === 0
        ? "not-applicable"
        : candidatesTruncated
          ? "partial"
          : "covered",
      request.settings.randomVariantCount === 0
        ? "Random variants were disabled."
        : `${request.settings.randomVariantCount.toLocaleString()} deterministic variant requests used seed hash ${seedHash}.`,
      request.settings.randomVariantCount,
    ),
  ];

  const positiveCandidates = candidates.filter(
    (candidate) => candidate.intendedOutcome === "match",
  );
  const negativeCandidates = candidates.filter(
    (candidate) => candidate.intendedOutcome === "no-match",
  );
  const orderedCandidates: GeneratedSubjectCandidate[] = [];
  const orderedLength = Math.max(
    positiveCandidates.length,
    negativeCandidates.length,
  );
  for (let index = 0; index < orderedLength; index += 1) {
    const positive = positiveCandidates[index];
    const negative = negativeCandidates[index];
    if (positive) orderedCandidates.push(positive);
    if (negative) orderedCandidates.push(negative);
  }

  return {
    generator: {
      id: GENERATED_CASE_GENERATOR_ID,
      version: GENERATED_CASE_GENERATOR_VERSION,
    },
    flavour: request.flavour,
    seed: request.seed,
    seedHash,
    candidates: orderedCandidates,
    coverage,
    unsupportedConstructs,
    visitedAstNodes: records.length,
    candidateBytes,
    candidateAttempts,
    duplicateCandidates,
    candidatesTruncated,
    traversalTruncated: false,
    ...(stoppedReason ? { stoppedReason } : {}),
    warnings,
  };
}
