import { DEFAULT_REGEX_LIMITS } from "../../../execution/request-limits";
import type { RegexDiagnostic } from "../../../model/diagnostics";
import type {
  CaptureDefinition,
  ReplacementSyntaxResult,
  ReplacementToken,
} from "../../../model/syntax";

const REPLACEMENT_REFERENCE =
  /\$\$|\$[&`'_+]|\$\d+|\$\{\*[^}]*\}|\$\*[A-Za-z_][A-Za-z0-9_]*|\$\{[^}]*\}|\$<[A-Za-z_][A-Za-z0-9_]*>|\$[A-Za-z_][A-Za-z0-9_]*/gu;

function literalToken(
  id: number,
  raw: string,
  start: number,
): ReplacementToken {
  return {
    id: `pcre2-replacement-${id}`,
    kind: "literal",
    raw,
    range: { startUtf16: start, endUtf16: start + raw.length },
    explanation: `Insert literal text ${JSON.stringify(raw.slice(0, 160))}${
      raw.length > 160 ? "…" : ""
    }`,
  };
}

function referenceToken(
  id: number,
  raw: string,
  start: number,
): ReplacementToken {
  const range = { startUtf16: start, endUtf16: start + raw.length };
  if (raw === "$$") {
    return {
      id: `pcre2-replacement-${id}`,
      kind: "escaped-dollar",
      raw,
      range,
      explanation: "Insert one literal dollar sign",
    };
  }
  if (raw === "$&" || raw === "$0") {
    return {
      id: `pcre2-replacement-${id}`,
      kind: "full-match",
      raw,
      range,
      explanation: "Insert the complete match",
    };
  }
  if (raw === "$`") {
    return {
      id: `pcre2-replacement-${id}`,
      kind: "prefix",
      raw,
      range,
      explanation: "Insert the subject text before this match",
    };
  }
  if (raw === "$'") {
    return {
      id: `pcre2-replacement-${id}`,
      kind: "suffix",
      raw,
      range,
      explanation: "Insert the subject text after this match",
    };
  }
  if (raw === "$_") {
    return {
      id: `pcre2-replacement-${id}`,
      kind: "subject",
      raw,
      range,
      explanation: "Insert the complete input subject",
    };
  }
  if (raw === "$+") {
    return {
      id: `pcre2-replacement-${id}`,
      kind: "highest-capture",
      raw,
      range,
      explanation: "Insert the highest-numbered capture that participated",
    };
  }
  if (
    /^(?:\$\*[A-Za-z_][A-Za-z0-9_]*|\$\{\*[A-Za-z_][A-Za-z0-9_]*\})$/u.test(raw)
  ) {
    return {
      id: `pcre2-replacement-${id}`,
      kind: "engine-special",
      raw,
      range,
      explanation:
        "Insert the last matching PCRE2 control-verb mark; the native output is authoritative because retained match DTOs do not expose marks",
    };
  }
  const number = /^\$(?:\{)?(\d+)\}?$/u.exec(raw)?.[1];
  if (number !== undefined) {
    const captureNumber = Number.parseInt(number, 10);
    return {
      id: `pcre2-replacement-${id}`,
      kind: captureNumber === 0 ? "full-match" : "numbered-capture",
      raw,
      range,
      explanation:
        captureNumber === 0
          ? "Insert the complete match"
          : `Insert numbered capture ${captureNumber}`,
      ...(captureNumber === 0 ? {} : { captureNumber }),
    };
  }
  const name =
    /^\$\{([^}]+)\}$/u.exec(raw)?.[1] ??
    /^\$<([^>]+)>$/u.exec(raw)?.[1] ??
    /^\$([A-Za-z_][A-Za-z0-9_]*)$/u.exec(raw)?.[1];
  if (name) {
    return {
      id: `pcre2-replacement-${id}`,
      kind: "named-capture",
      raw,
      range,
      explanation: `Insert named capture “${name}”`,
      captureName: name,
    };
  }
  return {
    id: `pcre2-replacement-${id}`,
    kind: "invalid",
    raw,
    range,
    explanation: "PCRE2 resolves this special replacement reference",
  };
}

export function parsePcre2Replacement(
  replacement: string,
  captures: readonly CaptureDefinition[],
): ReplacementSyntaxResult {
  if (
    replacement.length > DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16
  ) {
    return {
      accepted: false,
      tokens: [],
      diagnostics: [
        {
          id: "pcre2-replacement-template-input-limit",
          source: "syntax-provider",
          severity: "error",
          code: "replacement-template-limit",
          message: `Replacement template exceeds the ${DEFAULT_REGEX_LIMITS.maximumReplacementTemplateUtf16.toLocaleString()} UTF-16 unit limit; it was not parsed or executed.`,
          flavour: "pcre2",
          provenance: "derived",
        },
      ],
      totalDiagnostics: 1,
      diagnosticsTruncated: false,
    };
  }
  const tokens: ReplacementToken[] = [];
  const diagnostics: RegexDiagnostic[] = [];
  const numbers = new Set(captures.map((capture) => capture.number));
  const names = new Set(
    captures.flatMap((capture) => (capture.name ? [capture.name] : [])),
  );
  let cursor = 0;
  for (const match of replacement.matchAll(REPLACEMENT_REFERENCE)) {
    const start = match.index;
    const raw = match[0];
    if (start > cursor) {
      tokens.push(
        literalToken(tokens.length, replacement.slice(cursor, start), cursor),
      );
    }
    const token = referenceToken(tokens.length, raw, start);
    tokens.push(token);
    if (
      token.kind === "numbered-capture" &&
      token.captureNumber !== undefined &&
      !numbers.has(token.captureNumber)
    ) {
      diagnostics.push({
        id: `pcre2-replacement-missing-number-${start}`,
        source: "syntax-provider",
        severity: "warning",
        code: "replacement-unknown-capture",
        message: `Replacement references capture ${token.captureNumber}, which the partial lexical provider did not find. PCRE2 remains authoritative at execution.`,
        range: token.range,
        flavour: "pcre2",
        provenance: "derived",
      });
    } else if (
      token.kind === "named-capture" &&
      token.captureName !== undefined &&
      names.size > 0 &&
      !names.has(token.captureName)
    ) {
      diagnostics.push({
        id: `pcre2-replacement-missing-name-${start}`,
        source: "syntax-provider",
        severity: "warning",
        code: "replacement-unknown-capture",
        message: `Replacement references unknown named capture “${token.captureName}”.`,
        range: token.range,
        flavour: "pcre2",
        provenance: "derived",
      });
    } else if (token.kind === "engine-special") {
      diagnostics.push({
        id: `pcre2-replacement-mark-preview-${start}`,
        source: "syntax-provider",
        severity: "information",
        code: "replacement-preview-partial",
        message:
          "PCRE2 control-verb marks are evaluated by the native engine, but per-token preview mapping is unavailable because the retained match DTO does not expose the active mark.",
        range: token.range,
        flavour: "pcre2",
        provenance: "derived",
      });
    }
    cursor = start + raw.length;
  }
  if (cursor < replacement.length) {
    tokens.push(literalToken(tokens.length, replacement.slice(cursor), cursor));
  }
  return {
    accepted: true,
    tokens,
    diagnostics,
    totalDiagnostics: diagnostics.length,
    diagnosticsTruncated: false,
  };
}
