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

type SiblingGapMeasurement = {
  cell: MeasuredLayoutCellEvidence;
  edge: MeasuredLayoutEdge;
  source: MeasuredLayoutNode;
  target: MeasuredLayoutNode;
  parent: MeasuredLayoutNode;
  gap: number;
  tolerance: number;
  identity: string;
  partitionKey: string;
};

function compareText(left: string, right: string): number {
  return left.localeCompare(right, "en");
}

function gapAxis(axis: MeasuredLayoutAxis): "x" | "y" {
  if (axis === "x" || axis === "horizontal" || axis === "inline") return "x";
  return "y";
}

export function isSiblingGapEdge(edge: MeasuredLayoutEdge): boolean {
  return edge.relation === "sibling";
}

export function hasSharedParent(
  source: MeasuredLayoutNode,
  target: MeasuredLayoutNode,
  parent: MeasuredLayoutNode | undefined,
): parent is MeasuredLayoutNode {
  return parent !== undefined && source.parent !== null && source.parent === target.parent && source.parent === parent.identity;
}

export function flexNeighborsShareMeasuredLine(
  source: MeasuredLayoutNode,
  target: MeasuredLayoutNode,
): boolean {
  const sourceTopology = source.flexGridTopology;
  const targetTopology = target.flexGridTopology;
  if (sourceTopology.kind !== "flex" && targetTopology.kind !== "flex") return true;
  return sourceTopology.kind === "flex"
    && targetTopology.kind === "flex"
    && sourceTopology.flexLine !== null
    && sourceTopology.flexLine === targetTopology.flexLine;
}

export function isMasonryOrDensePackedGridWithoutProvableRowAdjacency(
  source: MeasuredLayoutNode,
  target: MeasuredLayoutNode,
  axis: MeasuredLayoutAxis,
): boolean {
  const sourceTopology = source.flexGridTopology;
  const targetTopology = target.flexGridTopology;
  if (sourceTopology.kind !== "grid" && targetTopology.kind !== "grid") return false;
  if (sourceTopology.kind !== "grid" || targetTopology.kind !== "grid") return true;
  return gapAxis(axis) !== "x"
    || sourceTopology.gridRow === null
    || sourceTopology.gridRow !== targetTopology.gridRow
    || sourceTopology.gridColumn === null
    || targetTopology.gridColumn === null;
}

export function isZeroModalGap(value: number): boolean {
  return value === 0;
}

function nodeIndexByIdentity(cell: MeasuredLayoutCellEvidence): Map<string, number> {
  return new Map(cell.measuredLayoutProbe.measuredLayoutGraph.nodes.map((node, index) => [node.identity, index]));
}

function jitterField(index: number, field: "x" | "y" | "width" | "height"): string {
  return `nodes[${String(index)}].borderBox.${field}`;
}

function gapTolerance(
  cell: MeasuredLayoutCellEvidence,
  edge: MeasuredLayoutEdge,
  source: MeasuredLayoutNode,
  target: MeasuredLayoutNode,
): number {
  const indexes = nodeIndexByIdentity(cell);
  const sourceIndex = indexes.get(source.identity);
  const targetIndex = indexes.get(target.identity);
  if (sourceIndex === undefined || targetIndex === undefined) {
    throw new Error("sibling gap edge references a node absent from its graph");
  }
  const axis = gapAxis(edge.axis);
  const fields = axis === "y"
    ? [[sourceIndex, "y"], [sourceIndex, "height"], [targetIndex, "y"]] as const
    : source.direction === "rtl"
      ? [[sourceIndex, "x"], [targetIndex, "x"], [targetIndex, "width"]] as const
      : [[sourceIndex, "x"], [sourceIndex, "width"], [targetIndex, "x"]] as const;
  return fields.reduce(
    (total, [index, field]) => total + toleranceFromJitter(cell.measuredLayoutProbe.jitterEnvelope, jitterField(index, field)),
    0,
  );
}

function adjacentChildSignature(source: MeasuredLayoutNode, target: MeasuredLayoutNode): string {
  return JSON.stringify([source.structuralSignature, target.structuralSignature]);
}

function partitionKey(
  cell: MeasuredLayoutCellEvidence,
  parent: MeasuredLayoutNode,
  edge: MeasuredLayoutEdge,
  source: MeasuredLayoutNode,
  target: MeasuredLayoutNode,
): string {
  return JSON.stringify({
    comparisonGroup: comparisonGroupKey(cell),
    parent: { cellId: cell.cell.id, identity: parent.identity },
    axis: edge.axis,
    adjacentChildStructuralSignature: adjacentChildSignature(source, target),
  });
}

function siblingGapMeasurements(cell: MeasuredLayoutCellEvidence): SiblingGapMeasurement[] {
  const nodes = cell.measuredLayoutProbe.measuredLayoutGraph.nodes;
  const nodesByIdentity = new Map(nodes.map((node) => [node.identity, node]));
  return cell.measuredLayoutProbe.measuredLayoutGraph.edges
    .filter(isSiblingGapEdge)
    .flatMap((edge) => {
      const source = nodesByIdentity.get(edge.source);
      const target = nodesByIdentity.get(edge.target);
      if (source === undefined || target === undefined) {
        throw new Error("sibling gap edge references an unknown graph node");
      }
      const parent = source.parent === null ? undefined : nodesByIdentity.get(source.parent);
      if (!hasSharedParent(source, target, parent)) return [];
      if (!flexNeighborsShareMeasuredLine(source, target)) return [];
      if (isMasonryOrDensePackedGridWithoutProvableRowAdjacency(source, target, edge.axis)) return [];
      const gap = edge.signedClearance;
      if (!Number.isFinite(gap)) throw new Error("sibling gap measurement must be finite");
      return [{
        cell,
        edge,
        source,
        target,
        parent,
        gap,
        tolerance: gapTolerance(cell, edge, source, target),
        identity: `${cell.cell.id}:${source.identity}->${target.identity}`,
        partitionKey: partitionKey(cell, parent, edge, source, target),
      }];
    })
    .sort((left, right) => compareText(left.identity, right.identity));
}

function modalTolerance(measurements: readonly SiblingGapMeasurement[]): number {
  if (measurements.length === 0) throw new Error("sibling gap partition must not be empty");
  return measurements.reduce((maximum, measurement) => Math.max(maximum, measurement.tolerance), 0);
}

export function detectRepeatedSiblingGapOutliers(
  matrix: MeasuredLayoutMatrixEvidence,
): MeasuredLayoutViolationFact[] {
  const partitions = new Map<string, SiblingGapMeasurement[]>();
  for (const cell of matrix.cells) {
    for (const measurement of siblingGapMeasurements(cell)) {
      const partition = partitions.get(measurement.partitionKey) ?? [];
      partition.push(measurement);
      partitions.set(measurement.partitionKey, partition);
    }
  }

  const violations: MeasuredLayoutViolationFact[] = [];
  for (const [key, unsortedMeasurements] of [...partitions.entries()].sort(([left], [right]) => compareText(left, right))) {
    const measurements = [...unsortedMeasurements].sort((left, right) => compareText(left.identity, right.identity));
    if (measurements.length < 3) continue;
    const mode = modalCluster(measurements.map((measurement) => measurement.gap), modalTolerance(measurements));
    if (mode === undefined || isZeroModalGap(mode.modalValue)) continue;
    const peerIdentities = mode.memberIndexes
      .map((index) => measurements[index]?.identity)
      .filter((identity): identity is string => identity !== undefined)
      .sort(compareText);
    const peerIndexes = new Set(mode.memberIndexes);
    for (let index = 0; index < measurements.length; index += 1) {
      const measurement = measurements[index];
      if (measurement === undefined || peerIndexes.has(index)) continue;
      violations.push({
        ruleId: "UI-092",
        kind: "repeated-sibling-gap-outlier",
        cellId: measurement.cell.cell.id,
        locator: measurement.identity,
        blamedIdentity: measurement.target.identity,
        summary: `sibling gap ${String(measurement.gap)} differs from modal gap ${String(mode.modalValue)}`,
        measurement: {
          laneEligibility: "advisory",
          rawMeasurement: measurement.gap,
          modalValue: mode.modalValue,
          peerIdentities,
          partitionKey: key,
          parentIdentity: measurement.parent.identity,
          axis: measurement.edge.axis,
          adjacentChildStructuralSignature: adjacentChildSignature(measurement.source, measurement.target),
          sourceIdentity: measurement.source.identity,
          targetIdentity: measurement.target.identity,
          jitterTolerance: modalTolerance(measurements),
        },
      });
    }
  }
  return violations.sort((left, right) => compareText(left.locator, right.locator));
}
