import type { LayoutShiftEntry } from "../../../playwright/src/cell-runner.js";
import type { MatrixCell } from "../../../schema/src/records/context.js";
import { CLS_GOOD_THRESHOLD, SCROLL_JUMP_TOLERANCE_PX } from "./constants.js";
import type {
  ActionCheckpointSnapshot,
  LayoutCellEvidence,
  LayoutViolationFact,
  RegionGeometry,
  TransitionCheckpointKind,
} from "./types.js";

export function performanceContractProofMet(cell: MatrixCell): boolean {
  return cell.state.uiPerformanceContract === "confirmed";
}

export function skeletonContractProofMet(cell: MatrixCell): boolean {
  return cell.state.uiSkeletonContract === "confirmed";
}

export function transitionContractProofMet(cell: MatrixCell): boolean {
  return cell.state.uiTransitionContract === "confirmed";
}

function largestShiftSourceLabel(layoutShifts: LayoutShiftEntry[]): string | undefined {
  let worst: { value: number; area: number; label: string } | undefined;

  for (const shift of layoutShifts) {
    for (const source of shift.sources) {
      const rect = source.currentRect.width > 0 ? source.currentRect : source.previousRect;
      const left = Math.min(source.previousRect.x, source.currentRect.x);
      const top = Math.min(source.previousRect.y, source.currentRect.y);
      const right = Math.max(
        source.previousRect.x + source.previousRect.width,
        source.currentRect.x + source.currentRect.width,
      );
      const bottom = Math.max(
        source.previousRect.y + source.previousRect.height,
        source.currentRect.y + source.currentRect.height,
      );
      const impactArea = (right - left) * (bottom - top);
      if (impactArea <= 0) {
        continue;
      }
      if (
        worst === undefined ||
        shift.value > worst.value ||
        (shift.value === worst.value && impactArea > worst.area)
      ) {
        worst = {
          value: shift.value,
          area: impactArea,
          label: `"${source.locator}" (${String(Math.round(rect.width))}×${String(Math.round(rect.height))}px)`,
        };
      }
    }
  }

  return worst?.label;
}

export function detectAggregateCls(evidence: LayoutCellEvidence): LayoutViolationFact[] {
  const probe = evidence.layoutProbe;
  if (probe.aggregateCls <= probe.clsThreshold) {
    return [];
  }

  const worstLabel = largestShiftSourceLabel(probe.layoutShifts);
  const cause =
    worstLabel === undefined
      ? "Content moved on its own after the page had already finished loading, so anything the reader was about to click shifted out from under them."
      : `Content moved on its own after the page had already finished loading, so anything the reader was about to click shifted out from under them. The largest movement was ${worstLabel}.`;
  const shiftCount = probe.layoutShifts.length;
  const shiftPhrase = shiftCount === 1 ? "1 movement" : `${String(shiftCount)} separate movements`;
  const overBy = (probe.aggregateCls / probe.clsThreshold).toFixed(1);
  const standard =
    probe.clsThreshold === CLS_GOOD_THRESHOLD
      ? `${overBy}x more movement than the Core Web Vitals industry standard allows (${probe.clsThreshold.toFixed(3)})`
      : `${overBy}x more movement than the configured limit allows (${probe.clsThreshold.toFixed(3)})`;

  return [
    {
      ruleId: "UI-030",
      kind: "aggregate-cls-exceeded",
      locator: "document",
      summary: `${cause} ${shiftPhrase} measured, ${probe.aggregateCls.toFixed(3)} in total: ${standard}.`,
      measurement: {
        aggregateCls: probe.aggregateCls,
        clsThreshold: probe.clsThreshold,
        layoutShiftCount: probe.layoutShifts.length,
        excludedRecentInputShiftCount: probe.excludedRecentInputShiftCount,
        layoutShifts: probe.layoutShifts,
        reportType: "aggregate-cls",
      },
    },
  ];
}

export function detectSkeletonSettledDiff(
  evidence: LayoutCellEvidence,
): LayoutViolationFact[] {
  return evidence.layoutProbe.skeletonSettledDiffs.map((diff) => ({
    ruleId: "UI-031",
    kind: "skeleton-shape-replacement",
    locator: diff.locator,
    summary: `skeleton geometry differs from settled content by ${String(diff.widthDelta)}x${String(diff.heightDelta)}px`,
    measurement: {
      skeletonRect: diff.skeletonRect,
      settledRect: diff.settledRect,
      widthDelta: diff.widthDelta,
      heightDelta: diff.heightDelta,
      shapeChanged: diff.shapeChanged,
      separateFromCls: true,
      reportType: "skeleton-geometry-diff",
      aggregateCls: evidence.layoutProbe.aggregateCls,
    },
  }));
}

function checkpointPairs(
  checkpoints: ActionCheckpointSnapshot[],
): Array<{ before: ActionCheckpointSnapshot; after: ActionCheckpointSnapshot }> {
  const pairs: Array<{ before: ActionCheckpointSnapshot; after: ActionCheckpointSnapshot }> = [];
  const byAction = new Map<string, ActionCheckpointSnapshot[]>();
  for (const snapshot of checkpoints) {
    const bucket = byAction.get(snapshot.actionId) ?? [];
    bucket.push(snapshot);
    byAction.set(snapshot.actionId, bucket);
  }
  for (const snapshots of byAction.values()) {
    const before = snapshots.find((entry) => entry.phase === "before");
    const after = snapshots.find((entry) => entry.phase === "after");
    if (before !== undefined && after !== undefined) {
      pairs.push({ before, after });
    }
  }
  return pairs;
}

function findRegion(regions: RegionGeometry[], locator: string): RegionGeometry | undefined {
  return regions.find((region) => region.locator === locator);
}

function detectSpinnerToContentReplacement(
  before: ActionCheckpointSnapshot,
  after: ActionCheckpointSnapshot,
): LayoutViolationFact | undefined {
  for (const beforeRegion of before.regions) {
    if (!beforeRegion.spinnerPresent) {
      continue;
    }
    const afterRegion = findRegion(after.regions, beforeRegion.locator);
    if (afterRegion === undefined) {
      continue;
    }
    if (afterRegion.spinnerPresent) {
      continue;
    }
    const identitySwapped =
      beforeRegion.stableId !== null &&
      afterRegion.stableId !== null &&
      beforeRegion.stableId !== afterRegion.stableId;
    const geometrySwapped =
      Math.abs(beforeRegion.rect.width - afterRegion.rect.width) > 2 ||
      Math.abs(beforeRegion.rect.height - afterRegion.rect.height) > 2;
    if (!identitySwapped && !geometrySwapped) {
      continue;
    }
    return {
      ruleId: "UI-032",
      kind: "spinner-to-content-replacement",
      locator: beforeRegion.locator,
      summary: "spinner replaced by differently shaped content instead of in-place update",
      measurement: {
        checkpointActionId: before.actionId,
        beforeRegion,
        afterRegion,
        identitySwapped,
        geometrySwapped,
      },
    };
  }
  return undefined;
}

function detectStateSwapInsteadOfUpdate(
  before: ActionCheckpointSnapshot,
  after: ActionCheckpointSnapshot,
): LayoutViolationFact | undefined {
  for (const beforeRegion of before.regions) {
    const afterRegion = findRegion(after.regions, beforeRegion.locator);
    if (afterRegion === undefined) {
      continue;
    }
    if (beforeRegion.stableId === null || afterRegion.stableId === null) {
      continue;
    }
    if (beforeRegion.stableId === afterRegion.stableId) {
      continue;
    }
    if (beforeRegion.textContent === afterRegion.textContent) {
      continue;
    }
    return {
      ruleId: "UI-032",
      kind: "state-swap-instead-of-update",
      locator: beforeRegion.locator,
      summary: "state changed via node replacement instead of in-place update",
      measurement: {
        checkpointActionId: before.actionId,
        beforeStableId: beforeRegion.stableId,
        afterStableId: afterRegion.stableId,
        beforeText: beforeRegion.textContent,
        afterText: afterRegion.textContent,
      },
    };
  }
  return undefined;
}

function detectFocusLoss(
  before: ActionCheckpointSnapshot,
  after: ActionCheckpointSnapshot,
): LayoutViolationFact | undefined {
  if (before.focusLocator === null) {
    return undefined;
  }
  if (after.beforeFocusTargetPresent === false) {
    return {
      ruleId: "UI-032",
      kind: "focus-loss",
      locator: before.focusLocator,
      summary: "focus lost after action checkpoint without expected restoration",
      measurement: {
        checkpointActionId: before.actionId,
        beforeFocusLocator: before.focusLocator,
        afterFocusLocator: after.focusLocator,
        beforeFocusTargetPresent: after.beforeFocusTargetPresent,
      },
    };
  }
  return undefined;
}

function detectScrollJump(
  before: ActionCheckpointSnapshot,
  after: ActionCheckpointSnapshot,
): LayoutViolationFact | undefined {
  const deltaY = Math.abs(after.scrollY - before.scrollY);
  const deltaX = Math.abs(after.scrollX - before.scrollX);
  if (deltaY <= SCROLL_JUMP_TOLERANCE_PX && deltaX <= SCROLL_JUMP_TOLERANCE_PX) {
    return undefined;
  }
  return {
    ruleId: "UI-032",
    kind: "scroll-jump",
    locator: "window",
    summary: `scroll position jumped by ${String(deltaX)}x${String(deltaY)}px during action checkpoint`,
    measurement: {
      checkpointActionId: before.actionId,
      beforeScrollX: before.scrollX,
      beforeScrollY: before.scrollY,
      afterScrollX: after.scrollX,
      afterScrollY: after.scrollY,
      deltaX,
      deltaY,
    },
  };
}

function detectStaleUiAfterMutation(
  before: ActionCheckpointSnapshot,
  after: ActionCheckpointSnapshot,
  cell: MatrixCell,
): LayoutViolationFact | undefined {
  const expectedVersion = cell.state.expectedMutationVersion;
  if (expectedVersion === undefined || expectedVersion.trim().length === 0) {
    return undefined;
  }
  if (after.mutationVersion === expectedVersion) {
    return undefined;
  }
  return {
    ruleId: "UI-032",
    kind: "stale-ui-after-mutation",
    locator: "[data-mutation-version]",
    summary: "UI shows stale mutation version after declared mutation completed",
    measurement: {
      checkpointActionId: before.actionId,
      expectedMutationVersion: expectedVersion,
      beforeMutationVersion: before.mutationVersion,
      afterMutationVersion: after.mutationVersion,
    },
  };
}

function detectMissingOptimisticRollback(
  before: ActionCheckpointSnapshot,
  after: ActionCheckpointSnapshot,
  cell: MatrixCell,
): LayoutViolationFact | undefined {
  const baseline = cell.state.optimisticBaselineValue;
  if (baseline === undefined || baseline.trim().length === 0) {
    return undefined;
  }
  if (!after.failureSignaled) {
    return undefined;
  }
  if (after.optimisticValue === baseline) {
    return undefined;
  }
  return {
    ruleId: "UI-032",
    kind: "missing-optimistic-rollback",
    locator: "[data-optimistic-value]",
    summary: "optimistic update not rolled back after mutation failure",
    measurement: {
      checkpointActionId: before.actionId,
      optimisticBaselineValue: baseline,
      beforeOptimisticValue: before.optimisticValue,
      afterOptimisticValue: after.optimisticValue,
      failureSignaled: after.failureSignaled,
    },
  };
}

export function detectActionCheckpointViolations(
  evidence: LayoutCellEvidence,
): LayoutViolationFact[] {
  const violations: LayoutViolationFact[] = [];
  for (const pair of checkpointPairs(evidence.layoutProbe.actionCheckpoints)) {
    const detectors = [
      detectSpinnerToContentReplacement(pair.before, pair.after),
      detectStateSwapInsteadOfUpdate(pair.before, pair.after),
      detectFocusLoss(pair.before, pair.after),
      detectScrollJump(pair.before, pair.after),
      detectStaleUiAfterMutation(pair.before, pair.after, evidence.cell),
      detectMissingOptimisticRollback(pair.before, pair.after, evidence.cell),
    ];
    for (const violation of detectors) {
      if (violation !== undefined) {
        violations.push(violation);
      }
    }
  }
  return violations;
}

export function detectTransitionCheckpoint(
  evidence: LayoutCellEvidence,
  kind: TransitionCheckpointKind,
): LayoutViolationFact[] {
  return detectActionCheckpointViolations(evidence).filter(
    (violation) => violation.kind === kind,
  );
}
