import { comparisonGroupKey, modalCluster, toleranceFromJitter } from "./partition.js";
import type {
  MeasuredLayoutCellEvidence,
  MeasuredLayoutMatrixEvidence,
  MeasuredLayoutNode,
  MeasuredLayoutViolationFact,
} from "./types.js";

type AlignmentLineKind = "logical-start" | "logical-end" | "centerline" | "first-line-baseline";

type AlignmentPeer = {
  cell: MeasuredLayoutCellEvidence;
  node: MeasuredLayoutNode;
  identity: string;
  partition: string;
};

type AlignmentMeasurement = AlignmentPeer & {
  line: AlignmentLineKind;
  value: number;
  tolerance: number;
};

type InlineCoordinateFields = {
  coordinate: string;
  size: string;
};

export function isStableAlignmentCapture(cell: MeasuredLayoutCellEvidence): boolean {
  const { measuredLayoutGraph: graph } = cell.measuredLayoutProbe;
  return graph.viewport.scrollX === 0 && graph.viewport.scrollY === 0 &&
    cell.measuredLayoutProbe.settledState.atRest &&
    !cell.measuredLayoutProbe.settledState.timedOut;
}

export function hasAlignmentComponentSignature(node: MeasuredLayoutNode): boolean {
  return node.componentIdentity !== null && node.componentIdentity.length > 0;
}

export function isAlignmentVisible(node: MeasuredLayoutNode): boolean {
  return node.visibility.visible && !node.visibility.zeroArea;
}

export function hasComparableStructuralSignature(node: MeasuredLayoutNode): boolean {
  return node.structuralSignature.length > 0;
}

function modalStructuralSignature(peers: readonly MeasuredLayoutNode[]): string | undefined {
  const counts = new Map<string, number>();
  for (const peer of peers) {
    if (!hasComparableStructuralSignature(peer)) continue;
    counts.set(peer.structuralSignature, (counts.get(peer.structuralSignature) ?? 0) + 1);
  }
  const modal = [...counts.entries()]
    .sort(([leftSignature, leftCount], [rightSignature, rightCount]) =>
      rightCount - leftCount || leftSignature.localeCompare(rightSignature, "en"),
    )[0];
  if (modal === undefined || modal[1] * 2 <= peers.length) return undefined;
  return modal[0];
}

export function isStructurallyDistinctAlignmentPeer(
  peer: MeasuredLayoutNode,
  peers: readonly MeasuredLayoutNode[],
): boolean {
  const modalSignature = modalStructuralSignature(peers);
  return modalSignature === undefined || peer.structuralSignature !== modalSignature;
}

function treeDepth(node: MeasuredLayoutNode, nodesByIdentity: ReadonlyMap<string, MeasuredLayoutNode>): number | undefined {
  let depth = 0;
  let parent = node.parent;
  const seen = new Set<string>([node.identity]);
  while (parent !== null) {
    if (seen.has(parent)) return undefined;
    seen.add(parent);
    const parentNode = nodesByIdentity.get(parent);
    if (parentNode === undefined) return undefined;
    depth += 1;
    parent = parentNode.parent;
  }
  return depth;
}

export function hasComparableAlignmentPartition(peer: AlignmentPeer): boolean {
  return peer.partition.length > 0;
}

function gridSpan(node: MeasuredLayoutNode): readonly [number | null, number | null] {
  return [node.flexGridTopology.gridRowSpan, node.flexGridTopology.gridColumnSpan];
}

function partitionKey(cell: MeasuredLayoutCellEvidence, node: MeasuredLayoutNode, depth: number): string {
  return JSON.stringify({
    comparisonGroup: comparisonGroupKey(cell),
    componentSignature: node.componentIdentity,
    treeDepth: depth,
    writingMode: node.writingMode,
    direction: node.direction,
    gridSpan: gridSpan(node),
    lineCount: node.textLineRects.length,
  });
}

function alignmentPeers(cell: MeasuredLayoutCellEvidence): AlignmentPeer[] {
  if (!isStableAlignmentCapture(cell)) return [];
  const nodesByIdentity = new Map(cell.measuredLayoutProbe.measuredLayoutGraph.nodes.map((node) => [node.identity, node]));
  return cell.measuredLayoutProbe.measuredLayoutGraph.nodes.flatMap((node) => {
    if (!hasAlignmentComponentSignature(node) || !isAlignmentVisible(node) || !hasComparableStructuralSignature(node)) return [];
    const depth = treeDepth(node, nodesByIdentity);
    if (depth === undefined) return [];
    const peer: AlignmentPeer = {
      cell,
      node,
      identity: `${cell.cell.id}:${node.identity}`,
      partition: partitionKey(cell, node, depth),
    };
    return hasComparableAlignmentPartition(peer) ? [peer] : [];
  });
}

function inlineCoordinates(node: MeasuredLayoutNode): { start: number; end: number; center: number } {
  const vertical = node.writingMode.startsWith("vertical");
  const start = vertical ? node.borderBox.y : node.borderBox.x;
  const size = vertical ? node.borderBox.height : node.borderBox.width;
  const physicalEnd = start + size;
  return {
    start: node.direction === "rtl" ? physicalEnd : start,
    end: node.direction === "rtl" ? start : physicalEnd,
    center: start + size / 2,
  };
}

function nodeIndexInGraph(cell: MeasuredLayoutCellEvidence, node: MeasuredLayoutNode): number | undefined {
  const index = cell.measuredLayoutProbe.measuredLayoutGraph.nodes.findIndex((candidate) => candidate.identity === node.identity);
  return index >= 0 ? index : undefined;
}

function inlineCoordinateFields(nodeIndex: number, node: MeasuredLayoutNode): InlineCoordinateFields {
  return node.writingMode.startsWith("vertical")
    ? { coordinate: `nodes[${String(nodeIndex)}].borderBox.y`, size: `nodes[${String(nodeIndex)}].borderBox.height` }
    : { coordinate: `nodes[${String(nodeIndex)}].borderBox.x`, size: `nodes[${String(nodeIndex)}].borderBox.width` };
}

function jitterTolerance(
  cell: MeasuredLayoutCellEvidence,
  contributions: readonly [field: string, multiplier: number][],
): number | undefined {
  const envelope = cell.measuredLayoutProbe.jitterEnvelope;
  let tolerance = 0;
  for (const [field, multiplier] of contributions) {
    if (envelope.maxDeltaByField[field] === undefined) return undefined;
    tolerance += toleranceFromJitter(envelope, field) * multiplier;
  }
  return tolerance;
}

function measurementsForPeer(peer: AlignmentPeer): AlignmentMeasurement[] {
  const nodeIndex = nodeIndexInGraph(peer.cell, peer.node);
  if (nodeIndex === undefined) return [];
  const coordinates = inlineCoordinates(peer.node);
  const fields = inlineCoordinateFields(nodeIndex, peer.node);
  const coordinateTolerance = jitterTolerance(peer.cell, [[fields.coordinate, 1]]);
  const edgeTolerance = jitterTolerance(peer.cell, [[fields.coordinate, 1], [fields.size, 1]]);
  const centerTolerance = jitterTolerance(peer.cell, [[fields.coordinate, 1], [fields.size, 0.5]]);
  if (coordinateTolerance === undefined || edgeTolerance === undefined || centerTolerance === undefined) return [];
  const logicalStartTolerance = peer.node.direction === "rtl" ? edgeTolerance : coordinateTolerance;
  const logicalEndTolerance = peer.node.direction === "rtl" ? coordinateTolerance : edgeTolerance;
  const measurements: AlignmentMeasurement[] = [
    { ...peer, line: "logical-start", value: coordinates.start, tolerance: logicalStartTolerance },
    { ...peer, line: "logical-end", value: coordinates.end, tolerance: logicalEndTolerance },
    { ...peer, line: "centerline", value: coordinates.center, tolerance: centerTolerance },
  ];
  const firstLine = [...peer.node.textLineRects].sort((left, right) => left.lineIndex - right.lineIndex)[0];
  const baselineField = `nodes[${String(nodeIndex)}].textLineRects[${String(firstLine?.lineIndex ?? 0)}].baseline`;
  const baselineTolerance = jitterTolerance(peer.cell, [[baselineField, 1]]);
  if (firstLine?.baseline !== null && firstLine?.baseline !== undefined && baselineTolerance !== undefined) {
    measurements.push({ ...peer, line: "first-line-baseline", value: firstLine.baseline, tolerance: baselineTolerance });
  }
  return measurements;
}

function byPartition(matrix: MeasuredLayoutMatrixEvidence): Map<string, AlignmentPeer[]> {
  const grouped = new Map<string, AlignmentPeer[]>();
  for (const cell of matrix.cells) {
    for (const peer of alignmentPeers(cell)) {
      const group = grouped.get(peer.partition) ?? [];
      group.push(peer);
      grouped.set(peer.partition, group);
    }
  }
  return grouped;
}

function structurallyComparablePeers(peers: readonly AlignmentPeer[]): AlignmentPeer[] {
  const nodes = peers.map((peer) => peer.node);
  return peers.filter((peer) => !isStructurallyDistinctAlignmentPeer(peer.node, nodes));
}

function byPartitionAndLine(matrix: MeasuredLayoutMatrixEvidence): Map<string, AlignmentMeasurement[]> {
  const grouped = new Map<string, AlignmentMeasurement[]>();
  for (const [partition, peers] of byPartition(matrix)) {
    for (const peer of structurallyComparablePeers(peers)) {
      for (const measurement of measurementsForPeer(peer)) {
        const key = `${partition}\u0000${measurement.line}`;
        const group = grouped.get(key) ?? [];
        group.push(measurement);
        grouped.set(key, group);
      }
    }
  }
  return grouped;
}

function compareMeasurements(left: AlignmentMeasurement, right: AlignmentMeasurement): number {
  return left.identity.localeCompare(right.identity, "en") || left.value - right.value;
}

export function detectAlignmentLineOutliers(matrix: MeasuredLayoutMatrixEvidence): MeasuredLayoutViolationFact[] {
  const violations: MeasuredLayoutViolationFact[] = [];
  for (const [partitionAndLine, measurements] of byPartitionAndLine(matrix)) {
    if (measurements.length < 3) continue;
    const ordered = [...measurements].sort(compareMeasurements);
    const cluster = modalCluster(ordered.map((measurement) => measurement.value), Math.max(...ordered.map((measurement) => measurement.tolerance)));
    if (cluster === undefined) continue;
    const peers = cluster.memberIndexes.map((index) => ordered[index]).filter((peer): peer is AlignmentMeasurement => peer !== undefined);
    const peerIdentities = peers.map((peer) => peer.identity).sort((left, right) => left.localeCompare(right, "en"));
    const [partition, line] = partitionAndLine.split("\u0000") as [string, AlignmentLineKind];
    for (let index = 0; index < ordered.length; index += 1) {
      if (cluster.memberIndexes.includes(index)) continue;
      const outlier = ordered[index];
      if (outlier === undefined) continue;
      violations.push({
        ruleId: "UI-094",
        kind: "alignment-line-outlier",
        cellId: outlier.cell.cell.id,
        locator: outlier.identity,
        blamedIdentity: outlier.node.identity,
        summary: `${line} differs from the observed modal alignment line by ${String(outlier.value - cluster.modalValue)} CSS pixels`,
        measurement: {
          rawMeasurement: outlier.value,
          modalValue: cluster.modalValue,
          peerIdentities,
          offsetFromModal: outlier.value - cluster.modalValue,
          line,
          partition,
          targetIdentity: outlier.identity,
          laneEligibility: "advisory",
        },
      });
    }
  }
  return violations.sort((left, right) => left.locator.localeCompare(right.locator, "en") || left.summary.localeCompare(right.summary, "en"));
}
