import type { RegexFlavourId } from "../model/flavour";
import type { NormalizedRegexNode } from "../model/syntax";
import type {
  RegexRiskConfidence,
  RegexRiskFinding,
  RegexRiskRule,
  RegexRiskSeverity,
  StaticRiskReport,
  StaticRiskRequest,
} from "./analysis.types";

export const MAXIMUM_STATIC_ANALYSIS_NODES = 20_000;
export const MAXIMUM_STATIC_RISK_FINDINGS = 100;
const EXCESSIVE_CAPTURE_THRESHOLD = 100;
const EXCESSIVE_NESTING_THRESHOLD = 32;
const MAXIMUM_FIRST_SIGNATURES = 8;

type FirstSignature =
  `literal:${string}` | `class:${string}` | "any" | "unknown";

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

interface NodeSummary {
  readonly containsRepeatableQuantifier: boolean;
  readonly firstRepeatableQuantifier?: NormalizedRegexNode;
  readonly firstBackreference?: NormalizedRegexNode;
  readonly firstWildcard?: NormalizedRegexNode;
  readonly firstAmbiguousAlternation?: NormalizedRegexNode;
  readonly firstSignatures: readonly FirstSignature[];
}

interface FindingDetails {
  readonly node: NormalizedRegexNode;
  readonly rule: RegexRiskRule;
  readonly title: string;
  readonly explanation: string;
  readonly exampleRisk: string;
  readonly confidence: RegexRiskConfidence;
  readonly severity: RegexRiskSeverity;
  readonly limitations: string;
  readonly suggestedInvestigation: string;
}

function isRepeatableQuantifier(node: NormalizedRegexNode): boolean {
  return (
    node.kind === "quantifier" &&
    (node.quantifier?.maximum === null || (node.quantifier?.maximum ?? 0) > 1)
  );
}

function isLookaround(node: NormalizedRegexNode): boolean {
  return (
    node.kind === "lookahead" ||
    node.kind === "negative-lookahead" ||
    node.kind === "lookbehind" ||
    node.kind === "negative-lookbehind"
  );
}

function limitedUnique<T>(values: readonly T[], maximum: number): readonly T[] {
  return [...new Set(values)].slice(0, maximum);
}

function ownFirstSignature(node: NormalizedRegexNode): FirstSignature[] {
  if (node.kind === "dot") return ["any"];
  if (node.kind === "literal") {
    return [`literal:${node.raw.slice(0, 2)}`];
  }
  if (node.kind === "escaped-literal") {
    if (/^\\[dDsSwWpP]/u.test(node.raw)) {
      return [`class:${node.raw.slice(0, 2)}`];
    }
    return [`literal:${node.raw}`];
  }
  if (
    node.kind === "character-class" ||
    node.kind === "character-class-range" ||
    node.kind === "unicode-property"
  ) {
    return [`class:${node.raw}`];
  }
  if (node.kind === "backreference") return ["unknown"];
  return [];
}

function signaturesOverlap(
  left: readonly FirstSignature[],
  right: readonly FirstSignature[],
): boolean {
  for (const leftSignature of left) {
    for (const rightSignature of right) {
      if (leftSignature === "unknown" || rightSignature === "unknown") {
        continue;
      }
      if (leftSignature === "any" || rightSignature === "any") return true;
      if (leftSignature === rightSignature) return true;
    }
  }
  return false;
}

function hasOverlappingChildren(
  node: NormalizedRegexNode,
  summaries: ReadonlyMap<NormalizedRegexNode, NodeSummary>,
): boolean {
  const alternativesContainer =
    node.kind === "disjunction" ||
    node.kind === "capture-group" ||
    node.kind === "named-capture-group" ||
    node.kind === "noncapture-group" ||
    node.kind === "inline-flags" ||
    isLookaround(node);
  if (!alternativesContainer || node.children.length < 2) return false;
  for (let leftIndex = 0; leftIndex < node.children.length; leftIndex += 1) {
    const left =
      summaries.get(node.children[leftIndex]!)?.firstSignatures ?? [];
    for (
      let rightIndex = leftIndex + 1;
      rightIndex < node.children.length;
      rightIndex += 1
    ) {
      const right =
        summaries.get(node.children[rightIndex]!)?.firstSignatures ?? [];
      if (signaturesOverlap(left, right)) return true;
    }
  }
  return false;
}

function firstSignaturesFor(
  node: NormalizedRegexNode,
  summaries: ReadonlyMap<NormalizedRegexNode, NodeSummary>,
): readonly FirstSignature[] {
  const own = ownFirstSignature(node);
  if (own.length > 0) return own;
  if (node.children.length === 0) return [];

  const isSequence = node.kind === "sequence" || node.kind === "alternative";
  if (isSequence) {
    const signatures: FirstSignature[] = [];
    for (const child of node.children) {
      signatures.push(...(summaries.get(child)?.firstSignatures ?? []));
      if (child.properties.nullable !== true) break;
    }
    return limitedUnique(signatures, MAXIMUM_FIRST_SIGNATURES);
  }

  return limitedUnique(
    node.children.flatMap(
      (child) => summaries.get(child)?.firstSignatures ?? [],
    ),
    MAXIMUM_FIRST_SIGNATURES,
  );
}

function collectRecords(root: NormalizedRegexNode): {
  readonly records: readonly NodeRecord[];
  readonly truncated: boolean;
} {
  const records: NodeRecord[] = [];
  const stack: NodeRecord[] = [{ node: root, depth: 0 }];
  while (stack.length > 0 && records.length < MAXIMUM_STATIC_ANALYSIS_NODES) {
    const record = stack.pop();
    if (!record) break;
    records.push(record);
    for (let index = record.node.children.length - 1; index >= 0; index -= 1) {
      const child = record.node.children[index];
      if (!child) continue;
      stack.push({
        node: child,
        depth: record.depth + 1,
        parent: record.node,
      });
    }
  }
  return { records, truncated: stack.length > 0 };
}

function buildSummaries(
  records: readonly NodeRecord[],
): ReadonlyMap<NormalizedRegexNode, NodeSummary> {
  const summaries = new Map<NormalizedRegexNode, NodeSummary>();
  for (let index = records.length - 1; index >= 0; index -= 1) {
    const node = records[index]?.node;
    if (!node) continue;
    const childSummaries = node.children
      .map((child) => summaries.get(child))
      .filter((summary): summary is NodeSummary => summary !== undefined);
    const overlapping = hasOverlappingChildren(node, summaries);
    summaries.set(node, {
      containsRepeatableQuantifier:
        isRepeatableQuantifier(node) ||
        childSummaries.some((summary) => summary.containsRepeatableQuantifier),
      firstRepeatableQuantifier:
        (isRepeatableQuantifier(node) ? node : undefined) ??
        childSummaries.find((summary) => summary.firstRepeatableQuantifier)
          ?.firstRepeatableQuantifier,
      firstBackreference:
        (node.kind === "backreference" ? node : undefined) ??
        childSummaries.find((summary) => summary.firstBackreference)
          ?.firstBackreference,
      firstWildcard:
        (node.kind === "dot" ? node : undefined) ??
        childSummaries.find((summary) => summary.firstWildcard)?.firstWildcard,
      firstAmbiguousAlternation:
        (overlapping ? node : undefined) ??
        childSummaries.find((summary) => summary.firstAmbiguousAlternation)
          ?.firstAmbiguousAlternation,
      firstSignatures: firstSignaturesFor(node, summaries),
    });
  }
  return summaries;
}

function finding(
  flavour: RegexFlavourId,
  sequence: number,
  details: FindingDetails,
): RegexRiskFinding {
  return {
    id: `risk-${details.rule}-${details.node.range.startUtf16}-${details.node.range.endUtf16}-${sequence}`,
    flavour,
    range: details.node.range,
    rule: details.rule,
    title: details.title,
    explanation: details.explanation,
    exampleRisk: details.exampleRisk,
    confidence: details.confidence,
    severity: details.severity,
    limitations: details.limitations,
    suggestedInvestigation: details.suggestedInvestigation,
    evidence: "static",
  };
}

function startsAtInput(root: NormalizedRegexNode): boolean {
  return root.raw.startsWith("^");
}

function severityOrder(severity: RegexRiskSeverity): number {
  if (severity === "high") return 0;
  if (severity === "warning") return 1;
  return 2;
}

function unsupportedReport(request: StaticRiskRequest): StaticRiskReport {
  return {
    flavour: request.flavour,
    analyser: {
      id: "regex-tools-ecmascript-heuristics",
      version: "1",
      support: "unsupported-flavour",
    },
    findings: [],
    analysedNodes: 0,
    findingsTruncated: false,
    traversalTruncated: false,
    summary: `Static risk analysis is unavailable for ${request.flavour}.`,
    limitations: [
      "The current analyser is scoped only to the normalized ECMAScript 2025 grammar.",
      "No ECMAScript heuristic is applied to another flavour.",
    ],
  };
}

export function analyseStaticRisk(
  request: StaticRiskRequest,
): StaticRiskReport {
  if (
    request.flavour !== "ecmascript" ||
    request.root.support.flavour !== "ecmascript"
  ) {
    return unsupportedReport(request);
  }

  const { records, truncated: traversalTruncated } = collectRecords(
    request.root,
  );
  const summaries = buildSummaries(records);
  const details: FindingDetails[] = [];
  let captureCount = 0;
  let deepest = records[0];
  const expensivePrefixCandidates: NormalizedRegexNode[] = [];

  for (const record of records) {
    const { node, depth } = record;
    if (node.capture) captureCount += 1;
    if (!deepest || depth > deepest.depth) deepest = record;
    const summary = summaries.get(node);
    if (!summary) continue;

    if (isRepeatableQuantifier(node)) {
      const body = node.children[0];
      const bodySummary = body ? summaries.get(body) : undefined;
      const innerQuantifier =
        bodySummary?.firstRepeatableQuantifier === node
          ? undefined
          : bodySummary?.firstRepeatableQuantifier;
      if (innerQuantifier) {
        details.push({
          node,
          rule: "nested-quantifier",
          title: "Potential nested-quantifier risk",
          explanation:
            "A repeated expression contains another repeatable expression. Some failing inputs can make a backtracking engine revisit the same partitions.",
          exampleRisk:
            "Long near-misses may take disproportionately longer than matching inputs.",
          confidence: "high",
          severity: "high",
          limitations:
            "This structural heuristic does not model engine optimizations, atomicity or every surrounding constraint.",
          suggestedInvestigation:
            "Run bounded growth analysis with a representative near-miss and consider removing one repetition layer.",
        });
        expensivePrefixCandidates.push(node);
      }
      if (body?.properties.nullable === true) {
        details.push({
          node,
          rule: "nullable-repeated-expression",
          title: "Potential nullable-repetition risk",
          explanation:
            "The repeated body can match an empty string, so many repetition paths can describe the same input position.",
          exampleRisk:
            "Backtracking work or zero-length iteration can grow without consuming input.",
          confidence: "high",
          severity: "high",
          limitations:
            "Native engines can simplify some nullable repetitions; this analyser does not inspect the compiled program.",
          suggestedInvestigation:
            "Require the body to consume input, or test the exact pattern under strict timeout limits.",
        });
        expensivePrefixCandidates.push(node);
      }
      if (bodySummary?.firstAmbiguousAlternation) {
        details.push({
          node,
          rule: "ambiguous-repeated-alternatives",
          title: "Potential ambiguous repeated alternatives",
          explanation:
            "Alternatives under this repetition can begin with overlapping input, leaving multiple possible partitions.",
          exampleRisk:
            "A long suffix failure can force the engine to reconsider many alternative boundaries.",
          confidence: "medium",
          severity: "high",
          limitations:
            "Overlap is approximated from first consuming constructs; later tokens and engine optimizations are not proven.",
          suggestedInvestigation:
            "Make alternatives prefix-distinct, factor shared prefixes, and probe representative near-misses.",
        });
        expensivePrefixCandidates.push(node);
      }
      if (bodySummary?.firstWildcard) {
        details.push({
          node: bodySummary.firstWildcard,
          rule: "repeated-wildcard",
          title: "Potential repeated-wildcard risk",
          explanation:
            "A wildcard occurs inside a repeated region and may consume the same text using multiple boundaries.",
          exampleRisk:
            "Later required text can trigger broad backtracking across the wildcard.",
          confidence: "medium",
          severity: "warning",
          limitations:
            "Anchors, following literals and engine search optimizations can substantially change the observed cost.",
          suggestedInvestigation:
            "Use a more specific character class or a bounded repetition, then compare bounded growth results.",
        });
        expensivePrefixCandidates.push(node);
      }
      if (bodySummary?.firstBackreference) {
        details.push({
          node: bodySummary.firstBackreference,
          rule: "backreference-in-repetition",
          title: "Potential repeated-backreference risk",
          explanation:
            "A backreference is evaluated inside repetition, coupling later work to previously captured text.",
          exampleRisk:
            "Multiple capture and repetition choices can amplify failing-input work.",
          confidence: "medium",
          severity: "warning",
          limitations:
            "The analyser does not resolve every capture path or prove that competing paths are reachable.",
          suggestedInvestigation:
            "Test realistic capture lengths and near-misses with the bounded growth runner.",
        });
        expensivePrefixCandidates.push(node);
      }
    }

    if (
      node.children.length > 1 &&
      summary.firstAmbiguousAlternation === node
    ) {
      details.push({
        node,
        rule: "overlapping-alternatives",
        title: "Possibly overlapping alternatives",
        explanation:
          "At least two alternatives can begin with the same approximated input class.",
        exampleRisk:
          "The engine may need to try more than one branch when later tokens fail.",
        confidence: "low",
        severity: "notice",
        limitations:
          "Only first-token overlap is checked. This is neither an equivalence test nor proof that costly backtracking occurs.",
        suggestedInvestigation:
          "Review whether the alternatives can be made prefix-distinct or whether their shared prefix can be factored out.",
      });
    }

    if (isLookaround(node) && summary.containsRepeatableQuantifier) {
      details.push({
        node,
        rule: "repeated-lookaround-body",
        title: "Potential expensive lookaround",
        explanation:
          "This lookaround evaluates a body containing repetition without consuming input at the assertion site.",
        exampleRisk:
          "An unanchored search may re-evaluate the repeated assertion at many candidate positions.",
        confidence: "medium",
        severity: "warning",
        limitations:
          "The analyser does not know which assertion results the runtime caches or optimizes.",
        suggestedInvestigation:
          "Anchor or narrow the assertion where possible and test a representative non-match.",
      });
      expensivePrefixCandidates.push(node);
    }
  }

  if (captureCount > EXCESSIVE_CAPTURE_THRESHOLD) {
    details.push({
      node: request.root,
      rule: "excessive-captures",
      title: "Large capture set",
      explanation: `The pattern contains ${captureCount.toLocaleString()} capturing groups, above this analyser’s ${EXCESSIVE_CAPTURE_THRESHOLD.toLocaleString()}-group review threshold.`,
      exampleRisk:
        "Capturing can increase result materialization, offset conversion and replacement work.",
      confidence: "high",
      severity: "notice",
      limitations:
        "The threshold is an application review limit, not an engine vulnerability boundary.",
      suggestedInvestigation:
        "Convert groups that are not consumed by results or backreferences to non-capturing groups.",
    });
  }

  if (deepest && deepest.depth > EXCESSIVE_NESTING_THRESHOLD) {
    details.push({
      node: deepest.node,
      rule: "excessive-nesting",
      title: "Deeply nested pattern",
      explanation: `The normalized tree reaches depth ${deepest.depth.toLocaleString()}, above this analyser’s ${EXCESSIVE_NESTING_THRESHOLD.toLocaleString()}-level review threshold.`,
      exampleRisk:
        "Deep nesting can increase compiler, stack or explanation work even when matching remains quick.",
      confidence: "high",
      severity: "notice",
      limitations:
        "Normalized AST depth is not identical to the native engine’s internal program depth.",
      suggestedInvestigation:
        "Simplify unnecessary grouping and retain strict compile and execution limits.",
    });
  }

  if (!startsAtInput(request.root) && expensivePrefixCandidates[0]) {
    details.push({
      node: expensivePrefixCandidates[0],
      rule: "unanchored-expensive-prefix",
      title: "Potential unanchored search amplification",
      explanation:
        "The pattern is not start-anchored and contains a structurally expensive region, so the engine may retry it at many subject positions.",
      exampleRisk:
        "A long non-match can multiply per-position backtracking by the subject length.",
      confidence: request.flags.includes("y") ? "low" : "medium",
      severity: "warning",
      limitations:
        "Sticky execution, runtime prefix scans and calling-code start offsets can avoid retries that are possible from the pattern alone.",
      suggestedInvestigation:
        "Use an appropriate start anchor or sticky execution when the intended match must begin at a known position.",
    });
  }

  const replacement = request.replacement ?? "";
  const repeatedExecution = request.scanAll || request.flags.includes("g");
  if (
    replacement.length > 0 &&
    repeatedExecution &&
    (replacement.includes("$`") ||
      replacement.includes("$'") ||
      replacement.length > 4_096)
  ) {
    details.push({
      node: request.root,
      rule: "potential-replacement-expansion",
      title: "Potentially large replacement output",
      explanation:
        replacement.includes("$`") || replacement.includes("$'")
          ? "The replacement refers to an entire subject prefix or suffix and can do so for every selected match."
          : "A long replacement template can be emitted once for every selected match.",
      exampleRisk:
        "Output size and replacement time can grow much faster than the input preview suggests.",
      confidence: "medium",
      severity: "warning",
      limitations:
        "The finding uses pattern and template structure; it does not predict the actual match count for every subject.",
      suggestedInvestigation:
        "Use bounded replacement previews and test output size on representative maximum inputs.",
    });
  }

  const seen = new Set<string>();
  const unique = details.filter((item) => {
    const key = `${item.rule}:${item.node.range.startUtf16}:${item.node.range.endUtf16}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
  unique.sort(
    (left, right) =>
      severityOrder(left.severity) - severityOrder(right.severity) ||
      left.node.range.startUtf16 - right.node.range.startUtf16 ||
      left.rule.localeCompare(right.rule),
  );
  const findingsTruncated = unique.length > MAXIMUM_STATIC_RISK_FINDINGS;
  const findings = unique
    .slice(0, MAXIMUM_STATIC_RISK_FINDINGS)
    .map((item, index) => finding(request.flavour, index + 1, item))
    .sort(
      (left, right) =>
        severityOrder(left.severity) - severityOrder(right.severity) ||
        left.range.startUtf16 - right.range.startUtf16 ||
        left.rule.localeCompare(right.rule),
    );

  return {
    flavour: request.flavour,
    analyser: {
      id: "regex-tools-ecmascript-heuristics",
      version: "1",
      support: "experimental-ecmascript-2025",
    },
    findings,
    analysedNodes: records.length,
    findingsTruncated,
    traversalTruncated,
    summary:
      findings.length === 0
        ? "No issue found by this analyser. This is not a safety guarantee."
        : `${findings.length.toLocaleString()} potential ${
            findings.length === 1 ? "risk" : "risks"
          } found by ECMAScript-specific structural heuristics.`,
    limitations: [
      "Findings are advisory and do not model the native engine’s complete compiled program or optimizations.",
      "Absence of a finding does not mean that a pattern is safe or linear.",
      "Dynamic probes characterize only the generated inputs and limits selected by the user.",
      ...(traversalTruncated
        ? [
            `Traversal stopped at ${MAXIMUM_STATIC_ANALYSIS_NODES.toLocaleString()} normalized nodes.`,
          ]
        : []),
      ...(findingsTruncated
        ? [
            `Findings stopped at ${MAXIMUM_STATIC_RISK_FINDINGS.toLocaleString()} entries.`,
          ]
        : []),
    ],
  };
}
