import type {
  CaptureResult,
  RegexExecutionResult,
  RegexMatchResult,
} from "../model/match";
import type {
  CaptureAlignment,
  ComparisonDifference,
  ComparisonDifferenceKind,
  ComparisonFlavour,
  ComparisonNotComparableReason,
  ComparisonSideOutcome,
  MatchAlignment,
  RegexComparisonRequest,
  RegexComparisonResult,
} from "./comparison.types";

const MAXIMUM_RETAINED_DIFFERENCES = 5_000;
const MAXIMUM_RETAINED_MATCH_ALIGNMENTS = 2_000;
const MAXIMUM_DIFFERENCE_VALUE_UTF16 = 1_024;

function differenceValue(value: string | undefined): string | undefined {
  if (value === undefined || value.length <= MAXIMUM_DIFFERENCE_VALUE_UTF16) {
    return value;
  }
  return `${value.slice(0, MAXIMUM_DIFFERENCE_VALUE_UTF16)}… (${value.length.toLocaleString()} UTF-16 units; complete value retained in its side result)`;
}

class DifferenceCollector {
  readonly retained: ComparisonDifference[] = [];
  total = 0;
  semanticTotal = 0;

  add(
    kind: ComparisonDifferenceKind,
    summary: string,
    left?: string,
    right?: string,
  ): void {
    this.total += 1;
    if (kind !== "effective-flags" && kind !== "offset-model") {
      this.semanticTotal += 1;
    }
    if (this.retained.length >= MAXIMUM_RETAINED_DIFFERENCES) return;
    this.retained.push({
      kind,
      summary,
      ...(left === undefined ? {} : { left }),
      ...(right === undefined ? {} : { right }),
    });
  }
}

function rangeKey(
  value:
    | {
        readonly range?: {
          readonly startUtf16: number;
          readonly endUtf16: number;
        };
      }
    | undefined,
): string | undefined {
  const range = value?.range;
  return range ? `${range.startUtf16}:${range.endUtf16}` : undefined;
}

function displayRange(
  value:
    | {
        readonly range?: {
          readonly startUtf16: number;
          readonly endUtf16: number;
        };
      }
    | undefined,
): string {
  const range = value?.range;
  return range ? `${range.startUtf16}–${range.endUtf16}` : "unavailable";
}

function captureIdentity(capture: CaptureResult): string {
  return capture.groupName
    ? `name:${capture.groupName}`
    : `number:${capture.groupNumber}`;
}

function takeMatching<T>(
  values: readonly T[],
  used: Set<number>,
  predicate: (value: T) => boolean,
): { readonly value: T; readonly index: number } | undefined {
  for (let index = 0; index < values.length; index += 1) {
    if (used.has(index)) continue;
    const value = values[index];
    if (value !== undefined && predicate(value)) return { value, index };
  }
  return undefined;
}

function captureEqual(left: CaptureResult, right: CaptureResult): boolean {
  return (
    left.groupNumber === right.groupNumber &&
    left.groupName === right.groupName &&
    left.status === right.status &&
    rangeKey(left) === rangeKey(right) &&
    left.value === right.value
  );
}

function compareCapture(
  left: CaptureResult | undefined,
  right: CaptureResult | undefined,
  alignment: CaptureAlignment["alignment"],
  differences: DifferenceCollector,
): CaptureAlignment {
  if (!left || !right) {
    differences.add(
      "capture-presence",
      "A capture is present on only one side.",
      left ? captureIdentity(left) : "absent",
      right ? captureIdentity(right) : "absent",
    );
    return {
      alignment,
      ...(left ? { left } : {}),
      ...(right ? { right } : {}),
      equal: false,
    };
  }
  if (
    left.groupNumber !== right.groupNumber ||
    left.groupName !== right.groupName
  ) {
    differences.add(
      "capture-identity",
      "Aligned captures have different group identities.",
      captureIdentity(left),
      captureIdentity(right),
    );
  }
  if (left.status !== right.status) {
    differences.add(
      "capture-status",
      "Aligned captures have different participation states.",
      left.status,
      right.status,
    );
  }
  if (rangeKey(left) !== rangeKey(right)) {
    differences.add(
      "capture-range",
      "Aligned captures have different normalized UTF-16 ranges.",
      displayRange(left),
      displayRange(right),
    );
  }
  if (left.value !== right.value) {
    differences.add(
      "capture-value",
      "Aligned captures have different retained values.",
      differenceValue(left.value) ?? "unavailable",
      differenceValue(right.value) ?? "unavailable",
    );
  }
  return {
    alignment,
    left,
    right,
    equal: captureEqual(left, right),
  };
}

function alignCaptures(
  left: readonly CaptureResult[],
  right: readonly CaptureResult[],
  differences: DifferenceCollector,
): readonly CaptureAlignment[] {
  const alignments: CaptureAlignment[] = [];
  const usedRight = new Set<number>();
  const deferredLeft: CaptureResult[] = [];

  for (const capture of left) {
    const key = rangeKey(capture);
    const sameRange = key
      ? (takeMatching(
          right,
          usedRight,
          (candidate) =>
            rangeKey(candidate) === key &&
            captureIdentity(candidate) === captureIdentity(capture),
        ) ??
        takeMatching(
          right,
          usedRight,
          (candidate) => rangeKey(candidate) === key,
        ))
      : undefined;
    if (!sameRange) {
      deferredLeft.push(capture);
      continue;
    }
    usedRight.add(sameRange.index);
    alignments.push(
      compareCapture(capture, sameRange.value, "normalized-range", differences),
    );
  }

  const stillDeferred: CaptureResult[] = [];
  for (const capture of deferredLeft) {
    const sameIdentity = takeMatching(
      right,
      usedRight,
      (candidate) => captureIdentity(candidate) === captureIdentity(capture),
    );
    if (!sameIdentity) {
      stillDeferred.push(capture);
      continue;
    }
    usedRight.add(sameIdentity.index);
    alignments.push(
      compareCapture(
        capture,
        sameIdentity.value,
        "capture-identity",
        differences,
      ),
    );
  }

  const remainingRight = right
    .map((value, index) => ({ value, index }))
    .filter(({ index }) => !usedRight.has(index));
  const paired = Math.min(stillDeferred.length, remainingRight.length);
  for (let index = 0; index < paired; index += 1) {
    const leftCapture = stillDeferred[index];
    const rightCapture = remainingRight[index];
    if (!leftCapture || !rightCapture) continue;
    usedRight.add(rightCapture.index);
    alignments.push(
      compareCapture(
        leftCapture,
        rightCapture.value,
        "ordinal-fallback",
        differences,
      ),
    );
  }
  for (const capture of stillDeferred.slice(paired)) {
    alignments.push(
      compareCapture(capture, undefined, "left-only", differences),
    );
  }
  for (const { value, index } of remainingRight.slice(paired)) {
    usedRight.add(index);
    alignments.push(
      compareCapture(undefined, value, "right-only", differences),
    );
  }
  return alignments;
}

function matchEqual(
  left: RegexMatchResult,
  right: RegexMatchResult,
  captures: readonly CaptureAlignment[],
): boolean {
  return (
    rangeKey(left) === rangeKey(right) &&
    left.value === right.value &&
    left.valueStatus === right.valueStatus &&
    captures.every((capture) => capture.equal)
  );
}

function compareMatch(
  left: RegexMatchResult | undefined,
  right: RegexMatchResult | undefined,
  alignment: MatchAlignment["alignment"],
  differences: DifferenceCollector,
): MatchAlignment {
  if (!left || !right) {
    differences.add(
      "match-presence",
      "A match is present on only one side.",
      left ? `match ${left.matchNumber} at ${displayRange(left)}` : "absent",
      right ? `match ${right.matchNumber} at ${displayRange(right)}` : "absent",
    );
    return {
      alignment,
      ...(left ? { left } : {}),
      ...(right ? { right } : {}),
      captures: [],
      equal: false,
    };
  }
  if (rangeKey(left) !== rangeKey(right)) {
    differences.add(
      "match-range",
      "Aligned matches have different normalized UTF-16 ranges.",
      displayRange(left),
      displayRange(right),
    );
  }
  if (left.value !== right.value || left.valueStatus !== right.valueStatus) {
    differences.add(
      "match-value",
      "Aligned matches have different retained values.",
      differenceValue(left.value),
      differenceValue(right.value),
    );
  }
  const captures = alignCaptures(left.captures, right.captures, differences);
  return {
    alignment,
    left,
    right,
    captures,
    equal: matchEqual(left, right, captures),
  };
}

function alignMatches(
  left: readonly RegexMatchResult[],
  right: readonly RegexMatchResult[],
  differences: DifferenceCollector,
): readonly MatchAlignment[] {
  const alignments: MatchAlignment[] = [];
  const usedRight = new Set<number>();
  const deferredLeft: RegexMatchResult[] = [];

  for (const match of left) {
    const key = rangeKey(match);
    const sameRange = takeMatching(
      right,
      usedRight,
      (candidate) => rangeKey(candidate) === key,
    );
    if (!sameRange) {
      deferredLeft.push(match);
      continue;
    }
    usedRight.add(sameRange.index);
    alignments.push(
      compareMatch(match, sameRange.value, "normalized-range", differences),
    );
  }

  const remainingRight = right
    .map((value, index) => ({ value, index }))
    .filter(({ index }) => !usedRight.has(index));
  const paired = Math.min(deferredLeft.length, remainingRight.length);
  for (let index = 0; index < paired; index += 1) {
    const leftMatch = deferredLeft[index];
    const rightMatch = remainingRight[index];
    if (!leftMatch || !rightMatch) continue;
    usedRight.add(rightMatch.index);
    alignments.push(
      compareMatch(
        leftMatch,
        rightMatch.value,
        "ordinal-fallback",
        differences,
      ),
    );
  }
  for (const match of deferredLeft.slice(paired)) {
    alignments.push(compareMatch(match, undefined, "left-only", differences));
  }
  for (const { value, index } of remainingRight.slice(paired)) {
    usedRight.add(index);
    alignments.push(compareMatch(undefined, value, "right-only", differences));
  }
  return alignments;
}

function executionFor(
  side: ComparisonSideOutcome,
): RegexExecutionResult | undefined {
  return side.runtime.replacement?.execution ?? side.runtime.execution;
}

function addReason(
  values: ComparisonNotComparableReason[],
  reason: ComparisonNotComparableReason,
): void {
  if (
    values.some(
      (candidate) =>
        candidate.code === reason.code &&
        candidate.flavour === reason.flavour &&
        candidate.message === reason.message,
    )
  ) {
    return;
  }
  values.push(reason);
}

function taskReasons(
  side: ComparisonSideOutcome,
  reasons: ComparisonNotComparableReason[],
): void {
  const flavour = side.input.flavour;
  if (side.syntax.status === "timeout") {
    addReason(reasons, {
      code: "syntax-timeout",
      flavour,
      message: `${flavour} syntax parsing timed out.`,
    });
  } else if (side.syntax.status !== "complete") {
    addReason(reasons, {
      code: "syntax-worker-failed",
      flavour,
      message: `${flavour} syntax parsing did not complete: ${side.syntax.error ?? side.syntax.status}.`,
    });
  } else if (
    side.syntax.pattern?.accepted === false ||
    side.syntax.replacement?.accepted === false
  ) {
    addReason(reasons, {
      code: "syntax-rejected",
      flavour,
      message: `${flavour} syntax provider rejected the exact pattern or replacement snapshot.`,
    });
  }

  if (side.runtime.status === "timeout") {
    addReason(reasons, {
      code: "execution-timeout",
      flavour,
      message: `${flavour} execution timed out; timeout is not a no-match result.`,
    });
    return;
  }
  if (side.runtime.status === "cancelled") {
    addReason(reasons, {
      code: "execution-cancelled",
      flavour,
      message: `${flavour} execution was cancelled and has no semantic result.`,
    });
    return;
  }
  if (side.runtime.status !== "complete") {
    addReason(reasons, {
      code: "execution-worker-failed",
      flavour,
      message: `${flavour} worker did not complete: ${side.runtime.error ?? side.runtime.status}.`,
    });
    return;
  }
  const execution = executionFor(side);
  if (!execution?.accepted) {
    addReason(reasons, {
      code: "compile-rejected",
      flavour,
      message: `${flavour} engine rejected the exact pattern snapshot.`,
    });
    return;
  }
  if (execution.truncated) {
    addReason(reasons, {
      code: "result-truncated",
      flavour,
      message: `${flavour} results reached a configured match or capture limit.`,
    });
  }
  if (
    execution.matches.some(
      (match) =>
        match.valueStatus === "truncated" ||
        match.captures.some((capture) => capture.status === "truncated"),
    )
  ) {
    addReason(reasons, {
      code: "value-truncated",
      flavour,
      message: `${flavour} retained value previews are incomplete.`,
    });
  }
  if (side.runtime.replacement?.truncated) {
    addReason(reasons, {
      code: "replacement-truncated",
      flavour,
      message: `${flavour} replacement output or its match collection is incomplete.`,
    });
  }
}

function compareMetadata(
  left: ComparisonSideOutcome,
  right: ComparisonSideOutcome,
  differences: DifferenceCollector,
): void {
  const leftSyntax = left.syntax.pattern;
  const rightSyntax = right.syntax.pattern;
  if (
    leftSyntax &&
    rightSyntax &&
    leftSyntax.accepted !== rightSyntax.accepted
  ) {
    differences.add(
      "syntax-acceptance",
      "The syntax providers disagree on acceptance.",
      leftSyntax.accepted ? "accepted" : "rejected",
      rightSyntax.accepted ? "accepted" : "rejected",
    );
  }
  const leftExecution = executionFor(left);
  const rightExecution = executionFor(right);
  if (
    leftExecution &&
    rightExecution &&
    leftExecution.accepted !== rightExecution.accepted
  ) {
    differences.add(
      "compile-acceptance",
      "The execution engines disagree on compilation.",
      leftExecution.accepted ? "accepted" : "rejected",
      rightExecution.accepted ? "accepted" : "rejected",
    );
  }
  if (
    leftExecution &&
    rightExecution &&
    leftExecution.flags.effectiveFlags !== rightExecution.flags.effectiveFlags
  ) {
    differences.add(
      "effective-flags",
      "The exact effective engine flags differ.",
      leftExecution.flags.effectiveFlags || "none",
      rightExecution.flags.effectiveFlags || "none",
    );
  }
  if (
    leftExecution &&
    rightExecution &&
    leftExecution.engine.offsetUnit !== rightExecution.engine.offsetUnit
  ) {
    differences.add(
      "offset-model",
      "Native offset units differ; alignments use normalized editor UTF-16 ranges.",
      leftExecution.engine.offsetUnit,
      rightExecution.engine.offsetUnit,
    );
  }
}

export function compareCompletedResults(
  request: RegexComparisonRequest,
  startedAt: string,
  elapsedMs: number,
  sides: readonly [ComparisonSideOutcome, ComparisonSideOutcome],
): RegexComparisonResult {
  const [left, right] = sides;
  const differences = new DifferenceCollector();
  const notComparable: ComparisonNotComparableReason[] = [];
  taskReasons(left, notComparable);
  taskReasons(right, notComparable);
  compareMetadata(left, right, differences);

  const leftExecution = executionFor(left);
  const rightExecution = executionFor(right);
  let allAlignments: readonly MatchAlignment[] = [];
  if (leftExecution?.accepted && rightExecution?.accepted) {
    if (leftExecution.matches.length !== rightExecution.matches.length) {
      differences.add(
        "match-count",
        "The engines returned different match counts.",
        leftExecution.matches.length.toLocaleString(),
        rightExecution.matches.length.toLocaleString(),
      );
    }
    allAlignments = alignMatches(
      leftExecution.matches,
      rightExecution.matches,
      differences,
    );
    const leftReplacement = left.runtime.replacement;
    const rightReplacement = right.runtime.replacement;
    if (
      leftReplacement &&
      rightReplacement &&
      leftReplacement.output !== rightReplacement.output
    ) {
      differences.add(
        "replacement-output",
        "The engine-native replacement outputs differ.",
        differenceValue(leftReplacement.output),
        differenceValue(rightReplacement.output),
      );
    }
  }

  const syntaxAcceptanceDiffers =
    left.syntax.pattern !== undefined &&
    right.syntax.pattern !== undefined &&
    left.syntax.pattern.accepted !== right.syntax.pattern.accepted;
  const compileAcceptanceDiffers =
    leftExecution !== undefined &&
    rightExecution !== undefined &&
    leftExecution.accepted !== rightExecution.accepted;
  if (syntaxAcceptanceDiffers || compileAcceptanceDiffers) {
    addReason(notComparable, {
      code: "flavour-specific-syntax",
      message:
        "At least one exact snapshot is accepted on only one side; no runtime equivalence claim is possible.",
    });
  }

  const status =
    notComparable.length > 0
      ? "not-comparable"
      : differences.semanticTotal === 0
        ? "same-for-current-input"
        : "different-for-current-input";
  const notices =
    status === "same-for-current-input"
      ? [
          {
            confidence: "likely-equivalent-for-input" as const,
            message:
              "The completed, untruncated results agree for this subject only. This is test evidence, not a proof that the patterns are equivalent.",
          },
        ]
      : status === "different-for-current-input"
        ? [
            {
              confidence: "requires-manual-review" as const,
              message:
                "The exact engine results differ for this subject. Review syntax, flags, ranges and captures before porting.",
            },
          ]
        : [
            {
              confidence:
                compileAcceptanceDiffers || syntaxAcceptanceDiffers
                  ? ("no-direct-equivalent" as const)
                  : ("requires-manual-review" as const),
              message:
                "The run is not comparable because one or more authoritative results are incomplete or rejected.",
            },
          ];

  return {
    schemaVersion: 1,
    request,
    startedAt,
    elapsedMs,
    sides,
    status,
    notComparable,
    differences: differences.retained,
    totalDifferences: differences.total,
    differencesTruncated: differences.retained.length < differences.total,
    matchAlignments: allAlignments.slice(0, MAXIMUM_RETAINED_MATCH_ALIGNMENTS),
    totalMatchAlignments: allAlignments.length,
    matchAlignmentsTruncated:
      allAlignments.length > MAXIMUM_RETAINED_MATCH_ALIGNMENTS,
    notices,
  };
}

export function comparisonSideLabel(flavour: ComparisonFlavour): string {
  return flavour === "ecmascript" ? "ECMAScript" : "PCRE2";
}
