import { DEFAULT_REGEX_LIMITS } from "../execution/request-limits";
import type { RegexSyntaxResult } from "../model/syntax";
import {
  ECMASCRIPT_FORMATTER_ID,
  ECMASCRIPT_FORMATTER_VERSION,
  type PatternFormatChange,
  type PatternFormatChangeKind,
  type PatternFormatPreview,
} from "./formatting.types";

const MAXIMUM_RETAINED_CHANGES = 10_000;

interface EscapeReplacement {
  readonly kind: PatternFormatChangeKind;
  readonly after: string;
  readonly explanation: string;
}

const CONTROL_ESCAPES: Readonly<Record<number, string>> = {
  0x00: "\\x00",
  0x08: "\\x08",
  0x09: "\\t",
  0x0a: "\\n",
  0x0b: "\\v",
  0x0c: "\\f",
  0x0d: "\\r",
} as const;

function hexEscape(codeUnit: number): string {
  return `\\x${codeUnit.toString(16).padStart(2, "0")}`;
}

function literalReplacement(raw: string): EscapeReplacement | undefined {
  const lastIndex = raw.length - 1;
  const codeUnit = raw.charCodeAt(lastIndex);
  if (codeUnit === 0x2f) {
    if (raw !== "/") return undefined;
    return {
      kind: "literal-slash",
      after: "\\/",
      explanation:
        "Escaped a literal slash so the pattern is safe to copy into an ECMAScript regex literal.",
    };
  }
  if (codeUnit === 0x2028 || codeUnit === 0x2029) {
    return {
      kind: "raw-line-terminator",
      after: `\\u${codeUnit.toString(16).padStart(4, "0")}`,
      explanation:
        "Replaced a raw ECMAScript line-separator code point with its equivalent Unicode escape.",
    };
  }
  if (codeUnit <= 0x1f || codeUnit === 0x7f) {
    return {
      kind: "raw-control",
      after: CONTROL_ESCAPES[codeUnit] ?? hexEscape(codeUnit),
      explanation:
        "Replaced a raw control code unit with its equivalent explicit ECMAScript escape.",
    };
  }
  return undefined;
}

interface LocatedReplacement extends EscapeReplacement {
  readonly startUtf16: number;
  readonly endUtf16: number;
  readonly before: string;
}

function replacementsFromSyntax(
  pattern: string,
  syntax: RegexSyntaxResult,
): readonly LocatedReplacement[] {
  return syntax.tokens
    .filter(
      (token) => token.kind === "literal" || token.kind === "escaped-literal",
    )
    .flatMap((token): readonly LocatedReplacement[] => {
      const before = pattern.slice(
        token.range.startUtf16,
        token.range.endUtf16,
      );
      if (before !== token.raw) return [];
      const replacement = literalReplacement(before);
      return replacement
        ? [
            {
              ...replacement,
              startUtf16: token.range.startUtf16,
              endUtf16: token.range.endUtf16,
              before,
            },
          ]
        : [];
    })
    .sort(
      (left, right) =>
        left.startUtf16 - right.startUtf16 || left.endUtf16 - right.endUtf16,
    );
}

function assertEligibleSyntax(
  pattern: string,
  syntax: RegexSyntaxResult,
): void {
  if (
    !syntax.accepted ||
    syntax.root.support.flavour !== "ecmascript" ||
    syntax.root.provenance.source === "recovered" ||
    syntax.root.raw !== pattern
  ) {
    throw new Error(
      "ECMAScript formatting requires the current accepted, non-recovered syntax-provider snapshot.",
    );
  }
  if (syntax.provider.id !== "regexpp-community") {
    throw new Error(
      `The active syntax provider ${syntax.provider.id} does not advertise this ECMAScript formatter.`,
    );
  }
}

/**
 * Canonicalizes only raw characters whose escape is context-independent.
 * Existing explicit escape spelling and every structural token remain byte-for-byte
 * untouched. Reparse/engine/test validation is a separate mandatory gate.
 */
export function formatEcmaScriptPattern(
  pattern: string,
  syntax: RegexSyntaxResult,
): PatternFormatPreview {
  assertEligibleSyntax(pattern, syntax);
  const output: string[] = [];
  const changes: PatternFormatChange[] = [];
  const replacements = replacementsFromSyntax(pattern, syntax);
  let totalChanges = 0;
  let cursor = 0;
  for (const replacement of replacements) {
    if (replacement.startUtf16 < cursor) continue;
    output.push(pattern.slice(cursor, replacement.startUtf16));
    output.push(replacement.after);
    cursor = replacement.endUtf16;
    totalChanges += 1;
    if (changes.length < MAXIMUM_RETAINED_CHANGES) {
      changes.push({
        kind: replacement.kind,
        startUtf16: replacement.startUtf16,
        endUtf16: replacement.endUtf16,
        before: replacement.before,
        after: replacement.after,
        explanation: replacement.explanation,
      });
    }
  }
  output.push(pattern.slice(cursor));
  const formattedPattern = output.join("");
  const withinPatternLimit =
    formattedPattern.length <= DEFAULT_REGEX_LIMITS.patternHardLengthUtf16;
  const warnings: string[] = [
    "Formatting preserves existing explicit escape spelling, grouping, alternation, quantifiers, inline flags and literal whitespace. It does not pretty-print by inserting whitespace or comments.",
    "Successful bounded validation is evidence for the checked subject and tests, not a proof of equivalence for every possible subject.",
  ];
  if (!withinPatternLimit) {
    warnings.push(
      `The formatted candidate is ${formattedPattern.length.toLocaleString()} UTF-16 units and exceeds the ${DEFAULT_REGEX_LIMITS.patternHardLengthUtf16.toLocaleString()} unit pattern limit.`,
    );
  }
  if (totalChanges > changes.length) {
    warnings.push(
      `Change details retain the first ${changes.length.toLocaleString()} of ${totalChanges.toLocaleString()} transformations.`,
    );
  }
  return {
    formatter: {
      id: ECMASCRIPT_FORMATTER_ID,
      version: ECMASCRIPT_FORMATTER_VERSION,
      flavour: "ecmascript",
      syntaxProvider: syntax.provider.id,
      syntaxProviderVersion: syntax.provider.version,
    },
    sourcePattern: pattern,
    formattedPattern,
    changed: totalChanges > 0,
    changes,
    totalChanges,
    changesTruncated: totalChanges > changes.length,
    withinPatternLimit,
    warnings,
  };
}
