import type {
  Detector,
  DetectorContext,
  DetectorOutcome,
  LaneEligibility,
} from "../../../core/src/sdk/detector.js";
import { isIntentionallyExcepted } from "./contracts.js";
import {
  clipContainmentContractProofMet,
  detectInteractiveTargetOccluded,
  detectTextGlyphsClipped,
  detectTruncatedWithoutAffordance,
  detectUnexpectedOverlap,
  detectVisibleContentOutsideClipChain,
  fullVisibilityContractProofMet,
  overlapContractProofMet,
  overlapIntentAmbiguous,
  visibilityContractProofMet,
} from "./detect.js";
import { buildUi001Evidence } from "./evidence.js";
import type {
  GeometryCellEvidence,
  GeometryRuleId,
  GeometryViolationFact,
  Ui001EvidenceRecord,
} from "./types.js";
import { GEOMETRY_DETECTOR_VERSION } from "./types.js";

type GeometryDetectorConfig = {
  id: string;
  className: string;
  ruleId: GeometryRuleId;
  defaultLane: LaneEligibility;
  detect: (evidence: GeometryCellEvidence) => GeometryViolationFact[];
  proofConditionMet?: (
    evidence: GeometryCellEvidence,
    context: DetectorContext,
  ) => boolean;
  classificationBasis: (violation: GeometryViolationFact) => string;
  ambiguousIntent?: (evidence: GeometryCellEvidence) => boolean;
};

function browserContext(cellId: string) {
  return { kind: "browser" as const, cell: { id: cellId } };
}

function buildOutcome(
  config: GeometryDetectorConfig,
  evidence: GeometryCellEvidence,
  violation: GeometryViolationFact,
  context: DetectorContext,
  proofConditionMet?: boolean,
): DetectorOutcome<Ui001EvidenceRecord> {
  const ui001 = buildUi001Evidence(
    evidence,
    config.ruleId,
    violation,
    config.classificationBasis(violation),
  );

  const effectiveProof =
    proofConditionMet ??
    (config.defaultLane === "blocking"
      ? true
      : config.defaultLane === "blocking-eligible"
        ? false
        : false);

  const truthSource =
    config.defaultLane === "advisory"
      ? "observed"
      : effectiveProof
        ? "universal"
        : "observed";

  const outcome: DetectorOutcome<Ui001EvidenceRecord> = {
    detector: { id: config.id, version: GEOMETRY_DETECTOR_VERSION },
    class: config.className,
    severity: "high",
    target: { kind: "dom", canonical: violation.locator },
    context: browserContext(evidence.cell.id),
    summary: violation.summary,
    evidence: [{ truthSource, payload: ui001 }],
    artifacts: [],
    laneEligibility: config.defaultLane,
    scope: {
      id: `${config.id}:${evidence.cell.id}:${violation.locator}`,
      detectorId: config.id,
      surfaceId: evidence.cell.id,
    },
    violation,
    contextDimensions: {
      route: evidence.cell.routeId,
      role: evidence.cell.role,
      locale: evidence.cell.locale,
    },
  };

  if (config.defaultLane === "blocking-eligible") {
    outcome.proofConditionMet = effectiveProof;
  }

  if (config.ambiguousIntent?.(evidence) === true) {
    outcome.laneEligibility = "advisory";
    outcome.evidence = [{ truthSource: "observed", payload: ui001 }];
    if (outcome.proofConditionMet !== undefined) {
      outcome.proofConditionMet = false;
    }
  }

  void context;
  return outcome;
}

function evaluateGeometryDetector(
  config: GeometryDetectorConfig,
  evidence: GeometryCellEvidence,
  context: DetectorContext,
): DetectorOutcome<Ui001EvidenceRecord>[] {
  if (isIntentionallyExcepted(context, evidence.cell, config.id, config.ruleId)) {
    return [];
  }

  const violations = config.detect(evidence);
  if (violations.length === 0) {
    return [];
  }

  return violations.map((violation) => {
    const proof =
      config.proofConditionMet === undefined
        ? undefined
        : config.proofConditionMet(evidence, context);
    return buildOutcome(config, evidence, violation, context, proof);
  });
}

function makeDetector(
  config: GeometryDetectorConfig,
): Detector<GeometryCellEvidence, Ui001EvidenceRecord> {
  return {
    id: config.id,
    version: GEOMETRY_DETECTOR_VERSION,
    defaultLane: config.defaultLane,
    evaluate(evidence, context) {
      return Promise.resolve(evaluateGeometryDetector(config, evidence, context));
    },
  };
}

export const textGlyphsClippedDetector = makeDetector({
  id: "ui-020-text-glyphs-clipped",
  className: "text-glyphs-clipped",
  ruleId: "UI-020",
  defaultLane: "blocking-eligible",
  detect: detectTextGlyphsClipped,
  proofConditionMet: (evidence) => visibilityContractProofMet(evidence.cell),
  classificationBasis: (violation) =>
    `glyph rect extends ${String(violation.measurement.extensionPx)}px beyond rounded clipping ancestor`,
});

export const interactiveTargetOccludedDetector = makeDetector({
  id: "ui-021-interactive-target-occluded",
  className: "interactive-target-occluded",
  ruleId: "UI-021",
  defaultLane: "blocking",
  detect: detectInteractiveTargetOccluded,
  classificationBasis: () =>
    "interior-point hit testing reaches foreign occluder while honoring pointer-events and stacking",
});

export const unexpectedOverlapDetector = makeDetector({
  id: "ui-022-unexpected-overlap",
  className: "unexpected-overlap",
  ruleId: "UI-022",
  defaultLane: "advisory",
  detect: detectUnexpectedOverlap,
  proofConditionMet: (evidence) => overlapContractProofMet(evidence.cell),
  classificationBasis: () =>
    "positive-area intersection between visible non-ancestor content; advisory until overlap contract forbids it",
  ambiguousIntent: (evidence) => overlapIntentAmbiguous(evidence.cell),
});

export const visibleContentOutsideClipChainDetector = makeDetector({
  id: "ui-023-visible-content-outside-clip-chain",
  className: "visible-content-outside-clip-chain",
  ruleId: "UI-023",
  defaultLane: "blocking-eligible",
  detect: detectVisibleContentOutsideClipChain,
  proofConditionMet: (evidence) => clipContainmentContractProofMet(evidence.cell),
  classificationBasis: (violation) =>
    `content crosses clip chain by ${String(violation.measurement.escapePx)}px`,
});

export const truncatedWithoutAffordanceDetector = makeDetector({
  id: "ui-024-truncated-without-affordance",
  className: "truncated-without-affordance",
  ruleId: "UI-024",
  defaultLane: "advisory",
  detect: detectTruncatedWithoutAffordance,
  proofConditionMet: (evidence) => fullVisibilityContractProofMet(evidence.cell),
  classificationBasis: () =>
    "truncated text lacks accessible full value or configured expansion behavior",
  ambiguousIntent: (evidence) => !fullVisibilityContractProofMet(evidence.cell),
});

export const geometryDetectors = [
  textGlyphsClippedDetector,
  interactiveTargetOccludedDetector,
  unexpectedOverlapDetector,
  visibleContentOutsideClipChainDetector,
  truncatedWithoutAffordanceDetector,
] as const;

export const geometryDetectorByRuleId: Record<
  GeometryRuleId,
  Detector<GeometryCellEvidence, Ui001EvidenceRecord>
> = {
  "UI-020": textGlyphsClippedDetector,
  "UI-021": interactiveTargetOccludedDetector,
  "UI-022": unexpectedOverlapDetector,
  "UI-023": visibleContentOutsideClipChainDetector,
  "UI-024": truncatedWithoutAffordanceDetector,
};
