import type {
  FindingPresentation,
  MarkerView,
  RelationGraphView,
  RelationRepresentationView,
  ReportArtifactRef,
  ReportExecutionContext,
  ReportFindingRecord,
  WitnessView,
  SharedChromeComparisonView,
} from "./types.js";

type ProvenancedEntry = {
  truthSource?: string;
  payload?: Record<string, unknown>;
};

type Rect = { x: number; y: number; width: number; height: number };

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

function formatValue(value: unknown): string {
  if (typeof value === "string") {
    return value;
  }
  return JSON.stringify(value);
}

function extractUi001Payload(evidence: unknown): Record<string, unknown> | undefined {
  if (!Array.isArray(evidence)) {
    return undefined;
  }
  const entries = evidence as ProvenancedEntry[];
  const preferred =
    entries.find((entry) => entry.truthSource === "observed") ??
    entries.find((entry) => entry.truthSource === "universal") ??
    entries[0];
  return preferred?.payload;
}

function flatRelationEvidence(evidence: unknown): Record<string, unknown> | undefined {
  if (!isRecord(evidence)) {
    return undefined;
  }
  if ("left" in evidence && "right" in evidence) {
    return evidence;
  }
  return undefined;
}

function readRect(value: unknown): Rect | undefined {
  if (!isRecord(value)) {
    return undefined;
  }
  const x = Number(value.x);
  const y = Number(value.y);
  const width = Number(value.width);
  const height = Number(value.height);
  if (
    !Number.isFinite(x) ||
    !Number.isFinite(y) ||
    !Number.isFinite(width) ||
    !Number.isFinite(height)
  ) {
    return undefined;
  }
  return { x, y, width, height };
}

function artifactByPath(
  artifacts: ReportArtifactRef[],
  relativePath: string,
): ReportArtifactRef | undefined {
  return artifacts.find((artifact) => artifact.relativePath === relativePath);
}

function screenshotOriginalArtifact(artifacts: ReportArtifactRef[]): ReportArtifactRef | undefined {
  return artifacts.find(
    (artifact) =>
      artifact.mediaType.startsWith("image/") &&
      !artifact.relativePath.includes("annotated") &&
      !artifact.relativePath.includes("crop") &&
      (artifact.relativePath.includes("screenshot") ||
        artifact.relativePath.includes("consistency-full") ||
        artifact.relativePath.includes("geometry-full")),
  );
}

function annotatedPath(artifacts: ReportArtifactRef[]): string | undefined {
  const match = artifacts.find(
    (artifact) =>
      artifact.relativePath.includes("annotated") &&
      (artifact.mediaType.includes("svg") || artifact.relativePath.endsWith(".svg")),
  );
  return match?.relativePath;
}

function cropPath(artifacts: ReportArtifactRef[], preferred?: string): string | undefined {
  if (preferred !== undefined && preferred.length > 0) {
    const explicit = artifactByPath(artifacts, preferred);
    if (explicit !== undefined) {
      return explicit.relativePath;
    }
  }
  const match = artifacts.find(
    (artifact) =>
      artifact.mediaType.startsWith("image/") &&
      (artifact.relativePath.includes("geometry-crop") ||
        artifact.relativePath.includes("crop")),
  );
  return match?.relativePath;
}

function markersFromPayload(payload: Record<string, unknown> | undefined): MarkerView[] {
  if (payload === undefined) {
    return [];
  }

  const measuredEvidence = isRecord(payload.measuredEvidence)
    ? payload.measuredEvidence
    : undefined;
  const geometryEvidence = isRecord(measuredEvidence?.geometryEvidence)
    ? measuredEvidence.geometryEvidence
    : undefined;
  const targetRect = readRect(geometryEvidence?.targetRect ?? measuredEvidence?.targetRect);
  if (targetRect !== undefined) {
    const label =
      typeof payload.targetLocator === "string"
        ? payload.targetLocator
        : "measured defect region";

    return [
      {
        index: 1,
        rect: targetRect,
        label,
        geometrySource: "measured",
      },
    ];
  }

  return shiftMarkers(measuredEvidence);
}

function sharedChromeComparison(
  payload: Record<string, unknown> | undefined,
  artifacts: ReportArtifactRef[],
): SharedChromeComparisonView | undefined {
  if (payload?.ruleId !== "UI-040" && payload?.ruleId !== "UI-041" && payload?.ruleId !== "UI-042" && payload?.ruleId !== "UI-043" && payload?.ruleId !== "UI-044") {
    return undefined;
  }
  const measured = isRecord(payload.measuredEvidence) ? payload.measuredEvidence : undefined;
  const comparison = isRecord(measured?.comparison) ? measured.comparison : undefined;
  const structuralDiff = Array.isArray(measured?.structuralDiff) ? measured.structuralDiff : undefined;
  if (comparison === undefined || structuralDiff === undefined) {
    return typeof measured?.visualComparisonUnavailable === "string"
      ? { referenceCount: 0, entries: [], unavailableReason: measured.visualComparisonUnavailable }
      : undefined;
  }
  const readSide = (side: "outlier" | "reference") => {
    const value = comparison[side];
    if (!isRecord(value) || typeof value.cellId !== "string" || !isRecord(value.memberGeometry)) {
      throw new Error(`shared chrome comparison ${side} is corrupt`);
    }
    const geometry: Record<string, Rect> = {};
    for (const [path, rect] of Object.entries(value.memberGeometry)) {
      const parsed = readRect(rect);
      if (parsed === undefined) {
        throw new Error(`shared chrome comparison ${side} geometry is corrupt`);
      }
      geometry[path] = parsed;
    }
    const path = typeof value.screenshotRef === "string" ? value.screenshotRef : undefined;
    return { geometry, artifact: path === undefined ? undefined : artifactByPath(artifacts, path) };
  };
  const outlier = readSide("outlier");
  const reference = readSide("reference");
  const entries = structuralDiff.map((value, index) => {
    if (!isRecord(value) || typeof value.path !== "string" || (value.kind !== undefined && typeof value.kind !== "string")) {
      throw new Error("shared chrome structural diff is corrupt");
    }
    const kind = value.kind;
    const outlierRect = kind === undefined || kind === "missing" || kind === "reordered" ? undefined : outlier.geometry[value.path];
    const referenceRect = kind === undefined || kind === "unexpected" || kind === "reordered" ? undefined : reference.geometry[value.path];
    const locations = [
      kind === "reordered" ? `same members appear in a different order: reference ${formatValue(value.dominant)}; this page ${formatValue(value.outlier)}` : "",
      kind === undefined ? "scalar value differences have no page element to outline" : "",
      outlierRect === undefined && kind !== undefined && kind !== "missing" && kind !== "reordered" ? "this page has no recorded geometry" : "",
      referenceRect === undefined && kind !== undefined && kind !== "unexpected" && kind !== "reordered" ? "reference page has no recorded geometry" : "",
    ].filter((entry) => entry.length > 0);
    return {
      index: index + 1,
      description: `${value.path}: this page ${formatValue(value.outlier)}; reference page ${formatValue(value.dominant)}${locations.length === 0 ? "" : `. No recorded geometry: ${locations.join("; ")}`}`,
      ...(outlierRect === undefined ? {} : { outlierRect }),
      ...(referenceRect === undefined ? {} : { referenceRect }),
    };
  });
  return {
    ...(outlier.artifact === undefined ? {} : { outlierImagePath: outlier.artifact.relativePath, outlierImageDimensions: outlier.artifact.dimensions }),
    ...(reference.artifact === undefined ? {} : { referenceImagePath: reference.artifact.relativePath, referenceImageDimensions: reference.artifact.dimensions }),
    referenceCount: 1,
    entries,
  };
}

function hasArea(rect: Rect | undefined): rect is Rect {
  return rect !== undefined && rect.width > 0 && rect.height > 0;
}

function unionImpactArea(before: Rect | undefined, after: Rect | undefined): number {
  if (!hasArea(before)) {
    return hasArea(after) ? after.width * after.height : 0;
  }
  if (!hasArea(after)) {
    return before.width * before.height;
  }
  const left = Math.min(before.x, after.x);
  const top = Math.min(before.y, after.y);
  const right = Math.max(before.x + before.width, after.x + after.width);
  const bottom = Math.max(before.y + before.height, after.y + after.height);
  return (right - left) * (bottom - top);
}

function movementLabel(locator: string, before: Rect | undefined, after: Rect | undefined): string {
  if (!hasArea(before)) {
    return `${locator} appeared here`;
  }
  if (!hasArea(after)) {
    return `${locator} disappeared from here`;
  }

  const directions: string[] = [];
  const changes: string[] = [];
  const dx = Math.round(after.x - before.x);
  const dy = Math.round(after.y - before.y);
  if (dx !== 0) {
    directions.push(`${String(Math.abs(dx))}px ${dx > 0 ? "right" : "left"}`);
  }
  if (dy !== 0) {
    directions.push(`${String(Math.abs(dy))}px ${dy > 0 ? "down" : "up"}`);
  }
  if (after.width !== before.width || after.height !== before.height) {
    const grew = after.width * after.height > before.width * before.height;
    changes.push(
      `${grew ? "grew" : "shrank"} to ${String(Math.round(after.width))}×${String(Math.round(after.height))}px`,
    );
  }

  const movement = directions.length === 0 ? "" : `moved ${directions.join(" and ")}`;
  const description = [movement, ...changes].filter((part) => part.length > 0).join(" and ");
  return description.length === 0 ? `${locator} shifted here` : `${locator} ${description}`;
}

function rectsDiffer(before: Rect, after: Rect): boolean {
  return before.x !== after.x ||
    before.y !== after.y ||
    before.width !== after.width ||
    before.height !== after.height;
}

function shiftMarkers(measuredEvidence: Record<string, unknown> | undefined): MarkerView[] {
  const shifts: unknown[] = Array.isArray(measuredEvidence?.layoutShifts)
    ? measuredEvidence.layoutShifts
    : [];

  const candidates: {
    rect: Rect;
    beforeRect?: Rect;
    label: string;
    shiftValue: number;
    impactArea: number;
  }[] = [];

  for (const shift of shifts.filter(isRecord)) {
    const shiftValue = typeof shift.value === "number" ? shift.value : 0;
    const sources: unknown[] = Array.isArray(shift.sources) ? shift.sources : [];
    for (const source of sources.filter(isRecord)) {
      const before = readRect(source.previousRect);
      const after = readRect(source.currentRect);
      const rect = hasArea(after) ? after : before;
      if (!hasArea(rect)) {
        continue;
      }
      const locator = typeof source.locator === "string" ? source.locator : "shifted region";
      candidates.push({
        rect,
        ...(hasArea(before) && hasArea(after) && rectsDiffer(before, after)
          ? { beforeRect: before }
          : {}),
        label: movementLabel(locator, before, after),
        shiftValue,
        impactArea: unionImpactArea(before, after),
      });
    }
  }

  return candidates
    .sort((left, right) => right.shiftValue - left.shiftValue || right.impactArea - left.impactArea)
    .map((candidate, position) => ({
      index: position + 1,
      rect: candidate.rect,
      ...(candidate.beforeRect === undefined ? {} : { beforeRect: candidate.beforeRect }),
      label: candidate.label,
      geometrySource: "derived",
      shiftValue: candidate.shiftValue,
    }));
}

function representationsFromViolation(
  violation: Record<string, unknown> | undefined,
): RelationRepresentationView[] {
  if (violation === undefined) {
    return [];
  }

  const facts = Array.isArray(violation.facts)
    ? (violation.facts as Array<{ nodeId?: string; normalizedValue?: unknown }>)
    : [];
  const measurement = isRecord(violation.measurement) ? violation.measurement : undefined;
  const representations = Array.isArray(measurement?.representations)
    ? (measurement.representations as Array<Record<string, unknown>>)
    : [];

  const byId = new Map<string, RelationRepresentationView>();

  for (const representation of representations) {
    const id = typeof representation.id === "string" ? representation.id : "node";
    byId.set(id, {
      id,
      source: typeof representation.source === "string" ? representation.source : "unknown",
      kind: typeof representation.kind === "string" ? representation.kind : "unknown",
      value: formatValue(representation.value),
      provenance:
        typeof representation.provenance === "string" ? representation.provenance : "observed",
    });
  }

  for (const fact of facts) {
    const nodeId = typeof fact.nodeId === "string" ? fact.nodeId : "node";
    const existing = byId.get(nodeId);
    if (existing === undefined) {
      byId.set(nodeId, {
        id: nodeId,
        source: nodeId,
        kind: "normalized",
        value: formatValue(fact.normalizedValue),
        provenance: "observed",
        normalizedValue: formatValue(fact.normalizedValue),
      });
      continue;
    }
    existing.normalizedValue = formatValue(fact.normalizedValue);
  }

  return [...byId.values()].sort((left, right) => left.id.localeCompare(right.id));
}

function relationGraphFromFlat(evidence: Record<string, unknown>): RelationGraphView {
  const left = isRecord(evidence.left) ? evidence.left : {};
  const right = isRecord(evidence.right) ? evidence.right : {};
  const representations: RelationRepresentationView[] = [
    {
      id: "left",
      source:
        typeof left.representation === "string" ? left.representation : "left-representation",
      kind: typeof left.representation === "string" ? left.representation : "left",
      value: formatValue(left.value),
      provenance:
        typeof evidence.provenance === "string" ? evidence.provenance : "observed",
    },
    {
      id: "right",
      source:
        typeof right.representation === "string" ? right.representation : "right-representation",
      kind: typeof right.representation === "string" ? right.representation : "right",
      value: formatValue(right.value),
      provenance:
        typeof evidence.provenance === "string" ? evidence.provenance : "observed",
    },
  ];

  return {
    relationId: typeof evidence.relation === "string" ? evidence.relation : "relation",
    relationKind: typeof evidence.relation === "string" ? evidence.relation : "relation",
    declaredBy: Array.isArray(evidence.universalIds)
      ? evidence.universalIds.map(String).join(", ")
      : "UNI-002",
    authorityBasis:
      typeof evidence.authorityBasis === "string" ? evidence.authorityBasis : "unknown",
    representations,
    noWinner: true,
  };
}

function relationGraphFromPayload(
  payload: Record<string, unknown> | undefined,
): RelationGraphView | undefined {
  if (payload === undefined) {
    return undefined;
  }

  const violation = isRecord(payload.violation) ? payload.violation : undefined;
  if (violation === undefined) {
    return undefined;
  }

  const representations = representationsFromViolation(violation);
  if (representations.length === 0) {
    return undefined;
  }

  const measurement = isRecord(violation.measurement) ? violation.measurement : undefined;
  const verdict = isRecord(measurement?.verdict) ? measurement.verdict : undefined;
  const facts = Array.isArray(verdict?.facts)
    ? (verdict.facts as Array<{ nodeId?: string; normalizedValue?: unknown }>)
    : Array.isArray(violation.facts)
      ? (violation.facts as Array<{ nodeId?: string; normalizedValue?: unknown }>)
      : [];

  for (const fact of facts) {
    const nodeId = typeof fact.nodeId === "string" ? fact.nodeId : "node";
    const existing = representations.find((entry) => entry.id === nodeId);
    if (existing !== undefined) {
      existing.normalizedValue = formatValue(fact.normalizedValue);
    }
  }

  return {
    relationId:
      typeof violation.relationId === "string" ? violation.relationId : "relation",
    relationKind: typeof violation.kind === "string" ? violation.kind : "relation",
    declaredBy: typeof violation.uniRule === "string" ? violation.uniRule : "UNI-002",
    authorityBasis:
      typeof violation.uniRule === "string"
        ? `${violation.uniRule} relation evaluation`
        : "relation evaluation",
    representations,
    noWinner: violation.noWinner === true || !("winner" in violation),
  };
}

function witnessesFromPayload(payload: Record<string, unknown> | undefined): WitnessView[] {
  if (payload === undefined) {
    return [];
  }

  const witnesses: WitnessView[] = [];
  const positiveWitness = isRecord(payload.positiveWitness) ? payload.positiveWitness : undefined;
  if (positiveWitness !== undefined) {
    witnesses.push({
      kind: "positive-control",
      description:
        typeof positiveWitness.description === "string"
          ? positiveWitness.description
          : "positive witness",
    });
  }

  const negativeWitness = isRecord(payload.negativeWitness) ? payload.negativeWitness : undefined;
  if (negativeWitness !== undefined) {
    witnesses.push({
      kind:
        typeof negativeWitness.kind === "string" ? negativeWitness.kind : "negative-witness",
      description:
        typeof negativeWitness.description === "string"
          ? negativeWitness.description
          : "negative witness",
    });
  }

  const witnessSet = isRecord(payload.witnessSet) ? payload.witnessSet : undefined;
  if (witnessSet !== undefined) {
    for (const key of ["precondition", "positiveControl", "calibration", "perturbation"]) {
      const entry = witnessSet[key];
      if (isRecord(entry)) {
        const witness: WitnessView = {
          kind: key,
          description: formatValue(entry),
        };
        if (typeof entry.status === "string") {
          witness.status = entry.status;
        }
        witnesses.push(witness);
      }
    }
  }

  return witnesses;
}

function executionContextFields(context: ReportExecutionContext): FindingPresentation["executionContext"] {
  if (context.kind === "browser") {
    return { kind: "browser" };
  }
  const fields: FindingPresentation["executionContext"] = {
    kind: context.kind,
    surface: context.surfaceId,
    adapter: context.adapterId,
    environment: context.environment,
    seed: context.seed,
  };
  if (context.action !== undefined) {
    fields.action = context.action.id;
  }
  return fields;
}

function browserContextFromPayload(payload: Record<string, unknown> | undefined) {
  if (payload === undefined) {
    return undefined;
  }
  const viewport = isRecord(payload.viewport) ? payload.viewport : undefined;
  if (viewport === undefined) {
    return undefined;
  }
  return {
    route: typeof payload.route === "string" ? payload.route : "",
    url: typeof payload.url === "string" ? payload.url : "",
    role: typeof payload.role === "string" ? payload.role : "",
    locale: typeof payload.locale === "string" ? payload.locale : "",
    viewport: {
      width: Number(viewport.width),
      height: Number(viewport.height),
      deviceScaleFactor: Number(viewport.deviceScaleFactor),
    },
    state: isRecord(payload.state)
      ? Object.fromEntries(
          Object.entries(payload.state).map(([key, value]) => [key, String(value)]),
        )
      : {},
  };
}

function exactReproductionFromPayload(
  payload: Record<string, unknown> | undefined,
  finding: ReportFindingRecord,
): string {
  if (payload !== undefined) {
    const reproduction = isRecord(payload.reproduction) ? payload.reproduction : undefined;
    if (reproduction !== undefined) {
      return JSON.stringify(reproduction, null, 2);
    }
    const parts = [
      typeof payload.route === "string" ? `route=${payload.route}` : undefined,
      typeof payload.url === "string" ? `url=${payload.url}` : undefined,
      typeof payload.role === "string" ? `role=${payload.role}` : undefined,
      typeof payload.locale === "string" ? `locale=${payload.locale}` : undefined,
      Array.isArray(payload.actions) ? `actions=${JSON.stringify(payload.actions)}` : undefined,
    ].filter((entry) => entry !== undefined);
    if (parts.length > 0) {
      return parts.join("\n");
    }
  }

  if (finding.context.kind !== "browser") {
    const ctx = finding.context;
    return [
      `kind=${ctx.kind}`,
      `surface=${ctx.surfaceId}`,
      `adapter=${ctx.adapterId}`,
      `seed=${ctx.seed}`,
    ].join("\n");
  }

  return `finding=${finding.id}\nrun=${finding.lastSeenRunId}`;
}

export function buildFindingPresentation(finding: ReportFindingRecord): FindingPresentation {
  const ui001 = extractUi001Payload(finding.evidence);
  const flatRelation = flatRelationEvidence(finding.evidence);

  const measuredEvidence = isRecord(ui001?.measuredEvidence) ? ui001.measuredEvidence : undefined;
  const geometryEvidence = isRecord(measuredEvidence?.geometryEvidence)
    ? measuredEvidence.geometryEvidence
    : undefined;

  const originalArtifact = screenshotOriginalArtifact(finding.artifacts);
  const originalPath = originalArtifact?.relativePath;
  const annotatedImagePath = annotatedPath(finding.artifacts);
  const cropImagePath = cropPath(
    finding.artifacts,
    typeof geometryEvidence?.targetCropRef === "string"
      ? geometryEvidence.targetCropRef
      : undefined,
  );

  const relationGraph =
    flatRelation !== undefined
      ? relationGraphFromFlat(flatRelation)
      : relationGraphFromPayload(ui001);

  const similarInstanceCount =
    typeof measuredEvidence?.instanceCount === "number"
      ? measuredEvidence.instanceCount
      : typeof measuredEvidence?.similarInstanceCount === "number"
        ? measuredEvidence.similarInstanceCount
        : undefined;

  const affectedContexts = Array.isArray(measuredEvidence?.affectedExecutionContexts)
    ? measuredEvidence.affectedExecutionContexts.map(String)
    : Array.isArray(ui001?.affectedExecutionContexts)
      ? ui001.affectedExecutionContexts.map(String)
      : undefined;

  const invariantOrContract =
    finding.contractIds.length > 0
      ? finding.contractIds.join(", ")
      : Array.isArray(flatRelation?.universalIds)
        ? flatRelation.universalIds.map(String).join(", ")
        : typeof ui001?.classificationBasis === "string"
          ? ui001.classificationBasis
          : "none referenced";

  const consoleEvidence =
    Array.isArray(ui001?.console) || Array.isArray(measuredEvidence?.console)
      ? JSON.stringify(ui001?.console ?? measuredEvidence?.console, null, 2)
      : undefined;

  const networkEvidence =
    Array.isArray(ui001?.network) || Array.isArray(measuredEvidence?.network)
      ? JSON.stringify(ui001?.network ?? measuredEvidence?.network, null, 2)
      : undefined;

  const traceEvidence =
    typeof ui001?.traceRef === "string"
      ? ui001.traceRef
      : typeof measuredEvidence?.traceRef === "string"
        ? measuredEvidence.traceRef
        : undefined;

  const browserCtx =
    finding.context.kind === "browser" ? browserContextFromPayload(ui001) : undefined;
  const comparison = sharedChromeComparison(ui001, finding.artifacts);

  return {
    id: finding.id,
    summary: finding.summary,
    className: finding.class,
    lane: finding.lane,
    certainty: finding.certainty,
    severity: finding.severity,
    executionContext: executionContextFields(finding.context),
    ...(browserCtx !== undefined ? { browserContext: browserCtx } : {}),
    imageStatus: finding.imageStatus,
    ...(originalPath !== undefined ? { originalImagePath: originalPath } : {}),
    ...(originalArtifact?.dimensions !== undefined
      ? { originalImageDimensions: originalArtifact.dimensions }
      : {}),
    ...(annotatedImagePath !== undefined ? { annotatedImagePath } : {}),
    ...(cropImagePath !== undefined ? { cropImagePath } : {}),
    markers: markersFromPayload(ui001),
    ...(comparison === undefined ? {} : { sharedChromeComparison: comparison }),
    detectorEvidence: JSON.stringify(
      ui001 ?? flatRelation ?? finding.evidence,
      null,
      2,
    ),
    ...(relationGraph !== undefined ? { relationGraph } : {}),
    witnesses: witnessesFromPayload(ui001),
    ...(consoleEvidence !== undefined ? { consoleEvidence } : {}),
    ...(networkEvidence !== undefined ? { networkEvidence } : {}),
    ...(traceEvidence !== undefined ? { traceEvidence } : {}),
    exactReproduction: exactReproductionFromPayload(ui001, finding),
    invariantOrContract,
    ...(similarInstanceCount !== undefined ? { similarInstanceCount } : {}),
    ...(affectedContexts !== undefined ? { affectedExecutionContexts: affectedContexts } : {}),
  };
}
