import { createHash } from "node:crypto";
import type { Page } from "playwright";
import type { MatrixCell } from "../../../schema/src/records/context.js";
import { hashFingerprint } from "../consistency/fingerprint.js";
import { capturePageScreenshot } from "../../../playwright/src/screenshot.js";
import type {
  DimensionObservation,
  LocaleSweepCheck,
  ResponsiveSweepCheck,
  SweepsArtifactRef,
  SweepsProbe,
} from "./types.js";

const LOCALE_CHECK_KINDS = new Set<string>([
  "text-expansion",
  "direction",
  "digit-text-spacing",
  "date-format",
  "number-format",
  "currency-format",
  "font-fallback",
  "bidi-isolation",
]);

const RESPONSIVE_CHECK_KINDS = new Set<string>([
  "breakpoint-boundary",
  "safe-area",
  "touch-target",
  "table-overlay",
]);

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

function parseLocaleCheck(value: unknown, index: number): LocaleSweepCheck {
  if (!isRecord(value)) {
    throw new Error(`localeChecks[${String(index)}] must be an object`);
  }
  if (typeof value.kind !== "string" || !LOCALE_CHECK_KINDS.has(value.kind)) {
    throw new Error(`localeChecks[${String(index)}].kind is invalid`);
  }
  if (typeof value.locator !== "string" || value.locator.trim().length === 0) {
    throw new Error(`localeChecks[${String(index)}].locator must be a non-empty string`);
  }
  if (typeof value.passed !== "boolean") {
    throw new Error(`localeChecks[${String(index)}].passed must be a boolean`);
  }
  const measurement = isRecord(value.measurement) ? value.measurement : { ...value };
  delete measurement.kind;
  delete measurement.locator;
  delete measurement.passed;
  return {
    kind: value.kind as LocaleSweepCheck["kind"],
    locator: value.locator,
    passed: value.passed,
    measurement,
  };
}

function parseResponsiveCheck(value: unknown, index: number): ResponsiveSweepCheck {
  if (!isRecord(value)) {
    throw new Error(`responsiveChecks[${String(index)}] must be an object`);
  }
  if (typeof value.kind !== "string" || !RESPONSIVE_CHECK_KINDS.has(value.kind)) {
    throw new Error(`responsiveChecks[${String(index)}].kind is invalid`);
  }
  if (typeof value.locator !== "string" || value.locator.trim().length === 0) {
    throw new Error(`responsiveChecks[${String(index)}].locator must be a non-empty string`);
  }
  if (typeof value.passed !== "boolean") {
    throw new Error(`responsiveChecks[${String(index)}].passed must be a boolean`);
  }
  const measurement = isRecord(value.measurement) ? value.measurement : { ...value };
  delete measurement.kind;
  delete measurement.locator;
  delete measurement.passed;
  return {
    kind: value.kind as ResponsiveSweepCheck["kind"],
    locator: value.locator,
    passed: value.passed,
    measurement,
  };
}

function parseObservation(value: unknown): DimensionObservation {
  if (!isRecord(value)) {
    throw new Error("observation must be an object");
  }
  if (typeof value.fingerprint !== "string" || value.fingerprint.trim().length === 0) {
    throw new Error("observation.fingerprint must be a non-empty string");
  }
  const signals: Record<string, string> = {};
  if (value.signals !== undefined) {
    if (!isRecord(value.signals)) {
      throw new Error("observation.signals must be an object");
    }
    for (const [key, signalValue] of Object.entries(value.signals)) {
      if (typeof signalValue !== "string") {
        throw new Error(`observation.signals.${key} must be a string`);
      }
      signals[key] = signalValue;
    }
  }
  return {
    fingerprint: value.fingerprint,
    signals,
  };
}

export function parseSweepsProbe(input: unknown): SweepsProbe {
  if (!isRecord(input)) {
    throw new Error("sweeps probe must be an object");
  }
  if (typeof input.sweepsVersion !== "string" || input.sweepsVersion.trim().length === 0) {
    throw new Error("sweepsVersion must be a non-empty string");
  }
  if (!Array.isArray(input.localeChecks)) {
    throw new Error("localeChecks must be an array");
  }
  if (!Array.isArray(input.responsiveChecks)) {
    throw new Error("responsiveChecks must be an array");
  }

  return {
    sweepsVersion: input.sweepsVersion,
    localeChecks: input.localeChecks.map((check, index) => parseLocaleCheck(check, index)),
    responsiveChecks: input.responsiveChecks.map((check, index) =>
      parseResponsiveCheck(check, index),
    ),
    observation: parseObservation(input.observation),
  };
}

export function parseEnabledDimensions(cell: MatrixCell): string[] {
  const raw = cell.state.uiSweepDimensions;
  if (raw === undefined || raw.trim().length === 0) {
    return [];
  }
  let parsed: unknown;
  try {
    parsed = JSON.parse(raw) as unknown;
  } catch {
    throw new Error("state.uiSweepDimensions must be valid JSON array");
  }
  if (!Array.isArray(parsed)) {
    throw new Error("state.uiSweepDimensions must be a JSON string array");
  }
  const dimensions: string[] = [];
  for (const entry of parsed) {
    if (typeof entry !== "string") {
      throw new Error("state.uiSweepDimensions must be a JSON string array");
    }
    dimensions.push(entry);
  }
  return dimensions.sort();
}

const DERIVED_SWEEPS_VERSION = "derived-1.0.0";

const INTERACTIVE_SELECTOR =
  "a[href], button, input, select, textarea, summary, [role='button'], [role='link'], [role='checkbox'], [role='radio'], [role='switch'], [role='tab'], [role='menuitem'], [role='menuitemcheckbox'], [role='menuitemradio'], [role='option'], [role='combobox'], [role='slider'], [role='spinbutton'], [role='treeitem']";

const TOUCH_TARGET_MIN_PX = 24;

async function readLiveChecks(page: Page): Promise<{
  expansion: Array<Record<string, unknown>>;
  touchTargets: Array<Record<string, unknown>>;
  signals: Record<string, string>;
}> {
  return page.evaluate(
    ({ interactiveSelector, minSize }) => {
      function locatorFor(element: Element): string {
        const segments: string[] = [];
        let current = element;
        for (;;) {
          if (current.id.length > 0) {
            segments.unshift(`#${current.id}`);
            return segments.join(" > ");
          }
          // Only `id` terminates the walk: a test id may repeat across rows, so
          // it narrows a segment without identifying an element on its own.
          const testId = current.getAttribute("data-testid");
          const parent = current.parentElement;
          const tag = current.tagName.toLowerCase();
          if (testId !== null && testId.length > 0) {
            segments.unshift(`[data-testid="${testId}"]`);
          } else if (parent === null) {
            segments.unshift(tag);
          } else {
            const self = current;
            const twins = Array.from(parent.children).filter(
              (sibling) => sibling.tagName === self.tagName,
            );
            segments.unshift(
              twins.length < 2 ? tag : `${tag}:nth-of-type(${String(twins.indexOf(self) + 1)})`,
            );
          }
          if (parent === null) {
            return segments.join(" > ");
          }
          current = parent;
        }
      }

      function ownText(element: Element): string {
        let text = "";
        for (const node of Array.from(element.childNodes)) {
          if (node.nodeType === Node.TEXT_NODE) {
            text += node.textContent ?? "";
          }
        }
        return text.trim();
      }

      const expansion: Array<Record<string, unknown>> = [];
      for (const element of Array.from(document.body.querySelectorAll("*"))) {
        const htmlElement = element as HTMLElement;
        if (ownText(htmlElement).length === 0) {
          continue;
        }
        const styles = window.getComputedStyle(htmlElement);
        if (paintsNothing(styles, htmlElement.getBoundingClientRect())) {
          continue;
        }
        if (htmlElement.clientWidth <= 1 || htmlElement.clientHeight <= 1) {
          continue;
        }
        const overflow = Math.round(htmlElement.scrollWidth - htmlElement.clientWidth);
        if (overflow <= 1) {
          continue;
        }
        const clipped = styles.overflowX === "hidden" || styles.overflowX === "clip";
        if (!clipped || styles.textOverflow === "ellipsis") {
          continue;
        }
        expansion.push({
          locator: locatorFor(htmlElement),
          contentWidth: htmlElement.scrollWidth,
          containerWidth: htmlElement.clientWidth,
          overflows: true,
        });
      }

      function lengthOf(token: string | undefined, basis: number): number {
        const value = Number.parseFloat(token ?? "");
        if (Number.isNaN(value)) {
          return 0;
        }
        return token !== undefined && token.endsWith("%") ? (value / 100) * basis : value;
      }

      // The visually-hidden idiom keeps a focusable box in the layout but clips
      // every painted pixel away, so the box accepts no pointer action.
      function paintsNothing(styles: CSSStyleDeclaration, rect: DOMRect): boolean {
        const clipPath = styles.clipPath;
        if (clipPath.startsWith("inset(")) {
          const parts = clipPath
            .slice("inset(".length, clipPath.indexOf(")"))
            .split(/\s+/)
            .filter((part) => part.length > 0 && part !== "round");
          if (parts.length > 0) {
            const top = parts[0];
            const right = parts.length > 1 ? parts[1] : top;
            const bottom = parts.length > 2 ? parts[2] : top;
            const left = parts.length > 3 ? parts[3] : right;
            if (
              lengthOf(top, rect.height) + lengthOf(bottom, rect.height) >= rect.height ||
              lengthOf(left, rect.width) + lengthOf(right, rect.width) >= rect.width
            ) {
              return true;
            }
          }
        }
        const clip = styles.getPropertyValue("clip");
        if (clip.startsWith("rect(")) {
          const parts = clip
            .slice("rect(".length, clip.indexOf(")"))
            .split(/[\s,]+/)
            .filter((part) => part.length > 0);
          if (parts.length === 4) {
            const top = lengthOf(parts[0], rect.height);
            const right = lengthOf(parts[1], rect.width);
            const bottom = lengthOf(parts[2], rect.height);
            const left = lengthOf(parts[3], rect.width);
            if (right - left <= 0 || bottom - top <= 0) {
              return true;
            }
          }
        }
        return false;
      }

      const targets: Array<{
        element: HTMLElement;
        locator: string;
        rect: DOMRect;
        undersized: boolean;
      }> = [];
      for (const element of Array.from(document.body.querySelectorAll(interactiveSelector))) {
        const htmlElement = element as HTMLElement;
        const styles = window.getComputedStyle(htmlElement);
        if (styles.display === "none" || styles.visibility === "hidden") {
          continue;
        }
        if (styles.display === "inline") {
          continue;
        }
        const rect = htmlElement.getBoundingClientRect();
        if (paintsNothing(styles, rect)) {
          continue;
        }
        if (rect.width === 0 || rect.height === 0) {
          continue;
        }
        targets.push({
          element: htmlElement,
          locator: locatorFor(htmlElement),
          rect,
          undersized: rect.width < minSize || rect.height < minSize,
        });
      }

      const radius = minSize / 2;
      function centerOf(rect: DOMRect): { x: number; y: number } {
        return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
      }
      function circlesIntersect(a: DOMRect, b: DOMRect): boolean {
        const first = centerOf(a);
        const second = centerOf(b);
        return Math.hypot(first.x - second.x, first.y - second.y) < minSize;
      }
      function circleIntersectsRect(circle: DOMRect, box: DOMRect): boolean {
        const center = centerOf(circle);
        const nearestX = Math.min(Math.max(center.x, box.left), box.right);
        const nearestY = Math.min(Math.max(center.y, box.top), box.bottom);
        return Math.hypot(center.x - nearestX, center.y - nearestY) < radius;
      }

      const touchTargets: Array<Record<string, unknown>> = [];
      for (const target of targets) {
        if (!target.undersized) {
          continue;
        }
        const crowder = targets.find((other) => {
          if (other === target) {
            return false;
          }
          if (
            other.element.contains(target.element) ||
            target.element.contains(other.element)
          ) {
            return false;
          }
          return other.undersized
            ? circlesIntersect(target.rect, other.rect)
            : circleIntersectsRect(target.rect, other.rect);
        });
        if (crowder === undefined) {
          continue;
        }
        touchTargets.push({
          locator: target.locator,
          width: Math.round(target.rect.width),
          height: Math.round(target.rect.height),
          minSize,
          crowdedBy: crowder.locator,
        });
      }

      const landmarks = Array.from(
        document.querySelectorAll("header, nav, main, footer, aside, [role]"),
      )
        .map((element) => element.getAttribute("role") ?? element.tagName.toLowerCase())
        .sort()
        .join(",");
      const headings = Array.from(document.querySelectorAll("h1, h2, h3, h4, h5, h6"))
        .map((element) => `${element.tagName.toLowerCase()}:${element.textContent.trim()}`)
        .join("|");

      return {
        expansion,
        touchTargets,
        signals: {
          direction: document.documentElement.getAttribute("dir") ?? "",
          language: document.documentElement.getAttribute("lang") ?? "",
          landmarks,
          headings,
          interactiveCount: String(document.body.querySelectorAll(interactiveSelector).length),
          textLength: String(document.body.textContent.trim().length),
          layoutWidth: String(document.documentElement.scrollWidth),
        },
      };
    },
    { interactiveSelector: INTERACTIVE_SELECTOR, minSize: TOUCH_TARGET_MIN_PX },
  );
}

export async function captureSweepsProbe(page: Page, cell: MatrixCell): Promise<SweepsProbe> {
  const raw = await page.evaluate(() => {
    const globalWindow = window as typeof window & {
      __invSweepsProbe?: () => unknown;
    };
    return globalWindow.__invSweepsProbe === undefined ? null : globalWindow.__invSweepsProbe();
  });

  const liveChecks = await readLiveChecks(page);

  const probe: SweepsProbe =
    raw === null
      ? {
          sweepsVersion: DERIVED_SWEEPS_VERSION,
          localeChecks: [],
          responsiveChecks: [],
          observation: {
            fingerprint: hashFingerprint(liveChecks.signals),
            signals: liveChecks.signals,
          },
        }
      : parseSweepsProbe(raw);

  const localeChecks = [...probe.localeChecks];
  for (const entry of liveChecks.expansion) {
    const overflows = entry.overflows === true;
    if (!overflows) {
      continue;
    }
    const locator =
      typeof entry.locator === "string" && entry.locator.length > 0
        ? entry.locator
        : "unknown";
    const existing = localeChecks.find(
      (check) => check.kind === "text-expansion" && check.locator === locator,
    );
    if (existing !== undefined) {
      existing.passed = false;
      existing.measurement = {
        ...existing.measurement,
        contentWidth: entry.contentWidth,
        containerWidth: entry.containerWidth,
        overflows: true,
        source: "measured",
      };
      continue;
    }
    localeChecks.push({
      kind: "text-expansion",
      locator,
      passed: false,
      measurement: {
        contentWidth: entry.contentWidth,
        containerWidth: entry.containerWidth,
        overflows: true,
        source: "measured",
      },
    });
  }

  const responsiveChecks = [...probe.responsiveChecks];
  for (const entry of liveChecks.touchTargets) {
    const locator =
      typeof entry.locator === "string" && entry.locator.length > 0
        ? entry.locator
        : "unknown";
    const existing = responsiveChecks.find(
      (check) => check.kind === "touch-target" && check.locator === locator,
    );
    if (existing !== undefined) {
      existing.passed = false;
      existing.measurement = {
        ...existing.measurement,
        width: entry.width,
        height: entry.height,
        minSize: entry.minSize,
        crowdedBy: entry.crowdedBy,
        source: "measured",
      };
      continue;
    }
    responsiveChecks.push({
      kind: "touch-target",
      locator,
      passed: false,
      measurement: {
        width: entry.width,
        height: entry.height,
        minSize: entry.minSize,
        crowdedBy: entry.crowdedBy,
        source: "measured",
      },
    });
  }

  void cell;
  return {
    ...probe,
    localeChecks,
    responsiveChecks,
  };
}

export async function captureSweepsArtifacts(
  page: Page,
  cellId: string,
): Promise<{ fullScreenshot: SweepsArtifactRef }> {
  const screenshot = await capturePageScreenshot(page);
  const contentHash = createHash("sha256").update(screenshot).digest("hex");
  const viewport = page.viewportSize();
  return {
    fullScreenshot: {
      relativePath: `artifacts/sweeps/${cellId}/full.png`,
      contentHash,
      mediaType: "image/png",
      ...(viewport === null
        ? {}
        : { dimensions: { width: viewport.width, height: viewport.height } }),
    },
  };
}
