import type { MatrixCell } from "../../../schema/src/records/context.js";
import type { NetworkEntry } from "../../../playwright/src/cell-runner.js";
import {
  DISABLED_CURSORS,
  LARGE_EMPTY_MIN_AREA,
  LARGE_EMPTY_MIN_DIMENSION,
} from "./constants.js";
import type {
  AssetsCellEvidence,
  AssetsViolationFact,
  ContainerObservation,
  DisabledStylingObservation,
  ImageObservation,
} from "./types.js";

export function requiredContentContractProofMet(cell: MatrixCell): boolean {
  return cell.state.uiRequiredContentContract === "confirmed";
}

export function disabledStylingContractProofMet(cell: MatrixCell): boolean {
  return cell.state.uiDisabledStylingContract === "confirmed";
}

function normalizeUrl(url: string): string {
  try {
    const parsed = new URL(url, "http://fixture.local");
    return `${parsed.pathname}${parsed.search}`;
  } catch {
    return url;
  }
}

function imageNetworkFailures(
  network: NetworkEntry[],
): Map<string, { status: number; url: string }> {
  const failures = new Map<string, { status: number; url: string }>();
  for (const entry of network) {
    if (entry.resourceType !== "image" && !entry.url.match(/\.(png|jpe?g|gif|webp|svg|avif)(\?|$)/i)) {
      continue;
    }
    const status = entry.status ?? 0;
    if (status >= 400 || status === 0) {
      failures.set(normalizeUrl(entry.url), { status, url: entry.url });
    }
  }
  return failures;
}

function requestFailureForImage(
  image: ImageObservation,
  failures: Map<string, { status: number; url: string }>,
): { status: number; url: string } | undefined {
  const candidates = [normalizeUrl(image.currentSrc), normalizeUrl(image.src)];
  for (const candidate of candidates) {
    const failure = failures.get(candidate);
    if (failure !== undefined) {
      return failure;
    }
  }
  return undefined;
}

function isVisibleFallback(image: ImageObservation): boolean {
  return (
    image.visible &&
    image.complete &&
    image.naturalWidth === 0 &&
    image.naturalHeight === 0 &&
    image.rect.width >= 16 &&
    image.rect.height >= 16 &&
    !image.decorative
  );
}

function classifyImageFailure(
  image: ImageObservation,
  requestFailure: { status: number; url: string } | undefined,
): string {
  if (requestFailure !== undefined) {
    return "request-failure";
  }
  if (!image.complete) {
    return "incomplete-load";
  }
  if (image.src.startsWith("data:") && image.naturalWidth === 0) {
    return "decode-failure";
  }
  if (image.naturalWidth === 0 || image.naturalHeight === 0) {
    return "zero-intrinsic-size";
  }
  if (isVisibleFallback(image)) {
    return "visible-fallback";
  }
  return "broken-image";
}

export function detectMissingBlockedImages(
  evidence: AssetsCellEvidence,
): AssetsViolationFact[] {
  const failures = imageNetworkFailures(evidence.network);
  const violations: AssetsViolationFact[] = [];

  for (const image of evidence.assetsProbe.images) {
    if (!image.visible || image.decorative) {
      continue;
    }

    const requestFailure = requestFailureForImage(image, failures);
    const broken =
      requestFailure !== undefined ||
      (image.complete && image.naturalWidth === 0 && image.naturalHeight === 0) ||
      isVisibleFallback(image);

    if (!broken) {
      continue;
    }

    const failureKind = classifyImageFailure(image, requestFailure);
    violations.push({
      ruleId: "UI-070",
      kind: failureKind,
      locator: image.locator,
      summary: `image ${image.locator} failed with ${failureKind.replaceAll("-", " ")}`,
      measurement: {
        failureKind,
        src: image.src,
        currentSrc: image.currentSrc,
        naturalWidth: image.naturalWidth,
        naturalHeight: image.naturalHeight,
        complete: image.complete,
        rect: image.rect,
        alt: image.alt,
        requestEvidence:
          requestFailure === undefined
            ? null
            : {
                url: requestFailure.url,
                status: requestFailure.status,
              },
        visibleFallback: isVisibleFallback(image),
      },
    });
  }

  return violations;
}

function isLargeEmptyContainer(container: ContainerObservation): boolean {
  if (!container.visible) {
    return false;
  }
  if (container.rect.width < LARGE_EMPTY_MIN_DIMENSION) {
    return false;
  }
  if (container.rect.height < LARGE_EMPTY_MIN_DIMENSION) {
    return false;
  }
  if (container.area < LARGE_EMPTY_MIN_AREA) {
    return false;
  }
  return container.visibleDescendantCount === 0;
}

export function detectLargeEmptyContainers(
  evidence: AssetsCellEvidence,
): AssetsViolationFact[] {
  return evidence.assetsProbe.containers
    .filter((container) => isLargeEmptyContainer(container))
    .map((container) => ({
      ruleId: "UI-071",
      kind: "large-empty-container",
      locator: container.locator,
      summary: `container ${container.locator} is large (${String(Math.round(container.rect.width))}x${String(Math.round(container.rect.height))}px) with no visible descendants`,
      measurement: {
        dimensions: container.rect,
        area: container.area,
        visibleDescendantCount: container.visibleDescendantCount,
        visibleDescendantLocators: container.visibleDescendantLocators,
        backgroundColor: container.backgroundColor,
        backgroundImage: container.backgroundImage,
        hasBackgroundMedia: container.hasBackgroundMedia,
        associatedFailures: container.associatedFailures,
      },
    }));
}

function hasAccessibleExplanation(observation: DisabledStylingObservation): boolean {
  if (observation.accessibleExplanation === null) {
    return false;
  }
  return observation.accessibleExplanation.trim().length > 0;
}

function siblingNormsDiffer(observation: DisabledStylingObservation): boolean {
  if (observation.siblingNorms.length === 0) {
    return false;
  }
  return observation.siblingNorms.some((sibling) => {
    if (sibling.semanticallyDisabled) {
      return false;
    }
    const opacityDelta = Math.abs(sibling.opacity - observation.opacity);
    const cursorDiffers =
      sibling.cursor !== observation.cursor &&
      (DISABLED_CURSORS.has(observation.cursor) || observation.cursor === "not-allowed");
    const pointerDiffers =
      sibling.pointerEvents === "auto" && observation.pointerEvents === "none";
    return opacityDelta >= 0.2 || cursorDiffers || pointerDiffers;
  });
}

export function detectUnexplainedDisabledStyling(
  evidence: AssetsCellEvidence,
): AssetsViolationFact[] {
  return evidence.assetsProbe.disabledStyling
    .filter((observation) => observation.unexplained)
    .map((observation) => ({
      ruleId: "UI-072",
      kind: "unexplained-disabled-styling",
      locator: observation.locator,
      summary: `element ${observation.locator} looks disabled without semantic disabled state or accessible explanation`,
      measurement: {
        opacity: observation.opacity,
        cursor: observation.cursor,
        pointerEvents: observation.pointerEvents,
        semanticallyDisabled: observation.semanticallyDisabled,
        accessibleExplanation: observation.accessibleExplanation,
        hasAccessibleExplanation: hasAccessibleExplanation(observation),
        siblingNorms: observation.siblingNorms,
        siblingNormsDiffer: siblingNormsDiffer(observation),
        looksDisabled: observation.looksDisabled,
      },
    }));
}

export function evaluateDisabledStyling(observation: DisabledStylingObservation): boolean {
  if (observation.semanticallyDisabled) {
    return false;
  }
  if (hasAccessibleExplanation(observation)) {
    return false;
  }
  if (!observation.looksDisabled) {
    return false;
  }
  return siblingNormsDiffer(observation);
}
