import type { MatrixCell } from "../../../schema/src/records/context.js";
import type {
  GeometryCellEvidence,
  GeometryViolationFact,
  GlyphClippingObservation,
} from "./types.js";
import { isScrollReachable } from "./reachability.js";

function geometryTolerance(cell: MatrixCell): number {
  const raw = cell.state.uiGeometryTolerancePx;
  if (raw === undefined || raw.trim().length === 0) {
    return 2;
  }
  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed < 0) {
    throw new Error("state.uiGeometryTolerancePx must be a non-negative number");
  }
  return parsed;
}

function groupGlyphClippingsByStructure(
  observations: GlyphClippingObservation[],
): GlyphClippingObservation[][] {
  const groups = new Map<string, GlyphClippingObservation[]>();
  for (const observation of observations) {
    const key = `${observation.clipAncestorStructuralLocator}::${observation.text}`;
    const bucket = groups.get(key) ?? [];
    bucket.push(observation);
    groups.set(key, bucket);
  }
  return [...groups.values()];
}

export function detectTextGlyphsClipped(
  evidence: GeometryCellEvidence,
): GeometryViolationFact[] {
  const groups = groupGlyphClippingsByStructure(evidence.geometryProbe.glyphClippings);
  const violations: GeometryViolationFact[] = [];
  // An element that clips several text runs is one clipping defect; emitting a
  // violation per run would give them one identity under DATA-021.
  const byLocator = new Map<string, GeometryViolationFact>();

  for (const group of groups) {
    for (const observation of group) {
      const emitted = byLocator.get(observation.locator);
      if (emitted !== undefined) {
        emitted.measurement.clippedRunCount = Number(emitted.measurement.clippedRunCount ?? 1) + 1;
        continue;
      }
      const violation: GeometryViolationFact = {
        ruleId: "UI-020",
        kind: "text-glyphs-clipped",
        locator: observation.locator,
        summary: `glyph rect extends ${String(observation.extensionPx)}px beyond rounded clipping ancestor`,
        measurement: {
          text: observation.text,
          extensionPx: observation.extensionPx,
          extensionSide: observation.extensionSide,
          clipAncestorLocator: observation.clipAncestorLocator,
          glyphRects: observation.glyphRects,
          instanceCount: group.length,
          structuralRowCount: evidence.geometryProbe.structuralRowCount,
          tolerancePx: geometryTolerance(evidence.cell),
          clipChain: observation.clipChain,
          occluderRects: observation.occluderRects,
          computedOverflow: observation.computedOverflow,
          direction: observation.direction,
          transformMatrices: observation.transformMatrices,
          deviceScaleFactor: evidence.geometryProbe.deviceScaleFactor,
          targetRect: observation.targetRect,
          clippedRunCount: 1,
        },
      };
      byLocator.set(observation.locator, violation);
      violations.push(violation);
    }
  }

  return violations;
}

function isExternalNestedFringeOcclusion(
  observation: GeometryCellEvidence["geometryProbe"]["occlusions"][number],
): boolean {
  const container = observation.targetScrollContainer;
  if (container === null || container === undefined || observation.occluderRects.length === 0) {
    return false;
  }
  const centerSamples = observation.samplePoints.filter((point) => point.role === "center");
  if (centerSamples.length === 0 || centerSamples.some((point) => !point.legitimate)) {
    return false;
  }
  return observation.occluderRects.every(
    (occluder) =>
      occluder.opacity === 0 &&
      !occluder.viewportPinned &&
      occluder.scrollContainerLocators !== undefined &&
      !occluder.scrollContainerLocators.includes(container.locator),
  );
}

export function detectInteractiveTargetOccluded(
  evidence: GeometryCellEvidence,
): GeometryViolationFact[] {
  return evidence.geometryProbe.occlusions
    .filter(
      (observation) =>
        !isExternalNestedFringeOcclusion(observation) &&
        !isScrollReachable({
          targetRect: observation.targetRect,
          targetPosition: observation.targetPosition,
          targetInRootScroller: observation.targetInRootScroller,
          targetScrollContainer: observation.targetScrollContainer,
          targetHasStickyAncestor: observation.targetHasStickyAncestor,
          occluders: observation.occluderRects,
          fixedOccluders: observation.fixedOccluders,
          scrollExtent: evidence.geometryProbe.scrollExtent,
        }),
    )
    .map((observation) => ({
      ruleId: "UI-021",
      kind: "interactive-target-occluded",
      locator: observation.locator,
      summary: "interactive target interior hit testing reaches foreign occluder",
      measurement: {
        samplePoints: observation.samplePoints,
        occluderRects: observation.occluderRects,
        fixedOccluders: observation.fixedOccluders,
        clipChain: observation.clipChain,
        computedOverflow: observation.computedOverflow,
        direction: observation.direction,
        transformMatrices: observation.transformMatrices,
        deviceScaleFactor: evidence.geometryProbe.deviceScaleFactor,
        targetRect: observation.targetRect,
        targetPosition: observation.targetPosition,
        targetInRootScroller: observation.targetInRootScroller,
        targetScrollContainer: observation.targetScrollContainer,
        targetHasStickyAncestor: observation.targetHasStickyAncestor,
        scrollExtent: evidence.geometryProbe.scrollExtent,
      },
    }));
}

export function detectUnexpectedOverlap(
  evidence: GeometryCellEvidence,
): GeometryViolationFact[] {
  return evidence.geometryProbe.overlaps
    .filter(
      (observation) =>
        !observation.viewportPinnedA &&
        !observation.viewportPinnedB &&
        !observation.sharedSvgRoot &&
        observation.textOcclusionProven,
    )
    .map((observation) => ({
    ruleId: "UI-022",
    kind: "unexpected-overlap",
    locator: `${observation.locatorA}::${observation.locatorB}`,
    summary: "visible non-ancestor content has positive-area intersection",
    measurement: {
      locatorA: observation.locatorA,
      locatorB: observation.locatorB,
      intersection: observation.intersection,
      area: observation.area,
      clipChain: observation.clipChain,
      occluderRects: observation.occluderRects,
      computedOverflow: observation.computedOverflow,
      direction: observation.direction,
      transformMatrices: observation.transformMatrices,
      deviceScaleFactor: evidence.geometryProbe.deviceScaleFactor,
      targetRect: observation.targetRect,
      positionA: observation.positionA,
      positionB: observation.positionB,
      viewportPinnedA: observation.viewportPinnedA,
      viewportPinnedB: observation.viewportPinnedB,
      sharedSvgRoot: observation.sharedSvgRoot,
      hasRenderedTextA: observation.hasRenderedTextA,
      hasRenderedTextB: observation.hasRenderedTextB,
      zIndexA: observation.zIndexA,
      zIndexB: observation.zIndexB,
      ownOpaquePaintA: observation.ownOpaquePaintA,
      ownOpaquePaintB: observation.ownOpaquePaintB,
      intersectionHitSamples: observation.intersectionHitSamples,
      textOcclusionProven: observation.textOcclusionProven,
    },
  }));
}

export function detectVisibleContentOutsideClipChain(
  evidence: GeometryCellEvidence,
): GeometryViolationFact[] {
  return evidence.geometryProbe.clipEscapes.map((observation) => ({
    ruleId: "UI-023",
    kind: "visible-content-outside-clip-chain",
    locator: observation.locator,
    summary: `content crosses clip chain by ${String(observation.escapePx)}px on ${observation.escapeSide}`,
    measurement: {
      escapeSide: observation.escapeSide,
      escapePx: observation.escapePx,
      contentRect: observation.contentRect,
      clipChain: observation.clipChain,
      occluderRects: observation.occluderRects,
      computedOverflow: observation.computedOverflow,
      direction: observation.direction,
      transformMatrices: observation.transformMatrices,
      deviceScaleFactor: evidence.geometryProbe.deviceScaleFactor,
      targetRect: observation.targetRect,
    },
  }));
}

export function detectTruncatedWithoutAffordance(
  evidence: GeometryCellEvidence,
): GeometryViolationFact[] {
  return evidence.geometryProbe.truncations.map((observation) => ({
    ruleId: "UI-024",
    kind: "truncated-without-affordance",
    locator: observation.locator,
    summary: "truncated text lacks accessible full value or expansion affordance",
    measurement: {
      visibleText: observation.visibleText,
      textOverflow: observation.textOverflow,
      overflow: observation.overflow,
      hasTitle: observation.hasTitle,
      hasAriaLabel: observation.hasAriaLabel,
      hasExpansionAffordance: observation.hasExpansionAffordance,
      clipChain: observation.clipChain,
      occluderRects: observation.occluderRects,
      computedOverflow: observation.computedOverflow,
      direction: observation.direction,
      transformMatrices: observation.transformMatrices,
      deviceScaleFactor: evidence.geometryProbe.deviceScaleFactor,
      targetRect: observation.targetRect,
    },
  }));
}

export function visibilityContractProofMet(cell: MatrixCell): boolean {
  return cell.state.uiVisibilityContractConfirmed === "true";
}

export function clipContainmentContractProofMet(cell: MatrixCell): boolean {
  return cell.state.uiClipContainmentContractConfirmed === "true";
}

export function fullVisibilityContractProofMet(cell: MatrixCell): boolean {
  return cell.state.uiFullVisibilityContractConfirmed === "true";
}

export function overlapContractProofMet(cell: MatrixCell): boolean {
  return cell.state.uiOverlapContractForbidden === "true";
}

export function overlapIntentAmbiguous(cell: MatrixCell): boolean {
  return cell.state.uiOverlapContractForbidden !== "true";
}
