import { createHash } from "node:crypto";
import type { Page } from "playwright";
import { layoutShiftObserverScript, type LayoutShiftEntry } from "@invariantum/playwright";
import type { MatrixCell } from "../../../schema/src/records/context.js";
import {
  CLS_GOOD_THRESHOLD,
  DEFAULT_GEOMETRY_TOLERANCE_PX,
} from "./constants.js";
import { capturePageScreenshot } from "../../../playwright/src/screenshot.js";
import type {
  ActionCheckpointSnapshot,
  LayoutArtifactRef,
  LayoutProbe,
  Rect,
  RegionGeometry,
  SkeletonObservation,
  SkeletonSettledDiff,
} from "./types.js";

export function layoutShiftInitScript(): string {
  return layoutShiftObserverScript();
}

function clsThreshold(cell: MatrixCell): number {
  const raw = cell.state.uiClsThreshold;
  if (raw === undefined || raw.trim().length === 0) {
    return CLS_GOOD_THRESHOLD;
  }
  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed < 0) {
    throw new Error("state.uiClsThreshold must be a non-negative number");
  }
  return parsed;
}

function geometryTolerance(cell: MatrixCell): number {
  const raw = cell.state.uiGeometryTolerancePx;
  if (raw === undefined || raw.trim().length === 0) {
    return DEFAULT_GEOMETRY_TOLERANCE_PX;
  }
  const parsed = Number(raw);
  if (!Number.isFinite(parsed) || parsed < 0) {
    throw new Error("state.uiGeometryTolerancePx must be a non-negative number");
  }
  return parsed;
}

function rectsDifferBeyondTolerance(
  left: Rect,
  right: Rect,
  tolerancePx: number,
): boolean {
  return (
    Math.abs(left.width - right.width) > tolerancePx ||
    Math.abs(left.height - right.height) > tolerancePx
  );
}

function computeSkeletonSettledDiffs(
  skeletons: SkeletonObservation[],
  settledRegions: RegionGeometry[],
  tolerancePx: number,
): SkeletonSettledDiff[] {
  const diffs: SkeletonSettledDiff[] = [];
  for (const skeleton of skeletons) {
    const settled = settledRegions.find((region) => region.locator === skeleton.locator);
    if (settled === undefined) {
      continue;
    }
    const widthDelta = settled.rect.width - skeleton.rect.width;
    const heightDelta = settled.rect.height - skeleton.rect.height;
    const shapeChanged = rectsDifferBeyondTolerance(skeleton.rect, settled.rect, tolerancePx);
    if (!shapeChanged) {
      continue;
    }
    diffs.push({
      locator: skeleton.locator,
      skeletonRect: skeleton.rect,
      settledRect: settled.rect,
      widthDelta,
      heightDelta,
      shapeChanged,
    });
  }
  return diffs;
}

function aggregateCls(layoutShifts: LayoutShiftEntry[]): {
  aggregateCls: number;
  excludedRecentInputShiftCount: number;
} {
  let aggregateCls = 0;
  let excludedRecentInputShiftCount = 0;
  for (const shift of layoutShifts) {
    if (shift.hadRecentInput) {
      excludedRecentInputShiftCount += 1;
      continue;
    }
    aggregateCls += shift.value;
  }
  return { aggregateCls, excludedRecentInputShiftCount };
}

async function readLayoutShifts(page: Page): Promise<LayoutShiftEntry[]> {
  return page.evaluate(() => {
    const globalWindow = window as typeof window & {
      __invLayoutShifts?: LayoutShiftEntry[];
    };
    return globalWindow.__invLayoutShifts ?? [];
  });
}

export async function captureSkeletonObservations(page: Page): Promise<SkeletonObservation[]> {
  return page.evaluate(() => {
    const skeletons = document.querySelectorAll("[data-layout-skeleton]");
    return Array.from(skeletons).map((element) => {
      const rect = element.getBoundingClientRect();
      const locator =
        element.id.length > 0
          ? `#${element.id}`
          : `[data-layout-skeleton="${element.getAttribute("data-layout-skeleton") ?? ""}"]`;
      return {
        locator,
        rect: {
          x: rect.x,
          y: rect.y,
          width: rect.width,
          height: rect.height,
        },
      };
    });
  });
}

async function captureRegionGeometries(page: Page): Promise<RegionGeometry[]> {
  return page.evaluate(() => {
    const regions = document.querySelectorAll("[data-checkpoint-region]");
    return Array.from(regions).map((element) => {
      const rect = element.getBoundingClientRect();
      const style = window.getComputedStyle(element);
      const locator =
        element.id.length > 0
          ? `#${element.id}`
          : `[data-checkpoint-region="${element.getAttribute("data-checkpoint-region") ?? ""}"]`;
      const spinner = element.querySelector("[data-layout-spinner], .layout-spinner, [aria-busy='true']");
      return {
        locator,
        rect: {
          x: rect.x,
          y: rect.y,
          width: rect.width,
          height: rect.height,
        },
        visible: style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0,
        textContent: element.textContent.trim(),
        stableId: element.getAttribute("data-stable-id"),
        spinnerPresent: spinner !== null,
      };
    });
  });
}

async function captureCheckpointSnapshot(
  page: Page,
  actionId: string,
  phase: "before" | "after",
  beforeFocusLocator: string | null = null,
): Promise<ActionCheckpointSnapshot> {
  return page.evaluate(
    ({ checkpointActionId, checkpointPhase, priorFocusLocator }) => {
      const active = document.activeElement;
      let focusLocator: string | null = null;
      if (active instanceof HTMLElement) {
        if (active.id.length > 0) {
          focusLocator = `#${active.id}`;
        } else if (active.getAttribute("data-checkpoint-focus") !== null) {
          focusLocator = `[data-checkpoint-focus="${active.getAttribute("data-checkpoint-focus") ?? ""}"]`;
        }
      }

      const regions = Array.from(document.querySelectorAll("[data-checkpoint-region]")).map((element) => {
        const rect = element.getBoundingClientRect();
        const style = window.getComputedStyle(element);
        const locator =
          element.id.length > 0
            ? `#${element.id}`
            : `[data-checkpoint-region="${element.getAttribute("data-checkpoint-region") ?? ""}"]`;
        const spinner = element.querySelector("[data-layout-spinner], .layout-spinner, [aria-busy='true']");
        return {
          locator,
          rect: {
            x: rect.x,
            y: rect.y,
            width: rect.width,
            height: rect.height,
          },
          visible:
            style.visibility !== "hidden" &&
            style.display !== "none" &&
            rect.width > 0 &&
            rect.height > 0,
          textContent: element.textContent.trim(),
          stableId: element.getAttribute("data-stable-id"),
          spinnerPresent: spinner !== null,
        };
      });

      const mutationHost = document.querySelector("[data-mutation-version]");
      const optimisticHost = document.querySelector("[data-optimistic-value]");
      const failureHost = document.querySelector("[data-mutation-failed='true']");
      const beforeFocusTargetPresent =
        checkpointPhase === "after" && priorFocusLocator !== null
          ? document.querySelector(priorFocusLocator) !== null
          : undefined;

      return {
        actionId: checkpointActionId,
        phase: checkpointPhase,
        focusLocator,
        ...(beforeFocusTargetPresent === undefined
          ? {}
          : { beforeFocusTargetPresent }),
        scrollX: window.scrollX,
        scrollY: window.scrollY,
        regions,
        mutationVersion: mutationHost?.getAttribute("data-mutation-version") ?? null,
        optimisticValue: optimisticHost?.getAttribute("data-optimistic-value") ?? null,
        failureSignaled: failureHost !== null,
      };
    },
    {
      checkpointActionId: actionId,
      checkpointPhase: phase,
      priorFocusLocator: beforeFocusLocator,
    },
  );
}

export async function captureLayoutProbe(
  page: Page,
  cell: MatrixCell,
  actionCheckpoints: ActionCheckpointSnapshot[],
  skeletonObservations: SkeletonObservation[],
): Promise<LayoutProbe> {
  const layoutShifts = await readLayoutShifts(page);
  const settledRegions = await captureRegionGeometries(page);
  const tolerancePx = geometryTolerance(cell);
  const skeletonSettledDiffs = computeSkeletonSettledDiffs(
    skeletonObservations,
    settledRegions,
    tolerancePx,
  );
  const cls = aggregateCls(layoutShifts);

  return {
    clsThreshold: clsThreshold(cell),
    layoutShifts,
    aggregateCls: cls.aggregateCls,
    excludedRecentInputShiftCount: cls.excludedRecentInputShiftCount,
    skeletonObservations,
    settledRegions,
    skeletonSettledDiffs,
    actionCheckpoints,
  };
}

export async function captureCheckpointBefore(
  page: Page,
  actionId: string,
): Promise<ActionCheckpointSnapshot> {
  return captureCheckpointSnapshot(page, actionId, "before");
}

export async function captureCheckpointAfter(
  page: Page,
  actionId: string,
  beforeFocusLocator: string | null,
): Promise<ActionCheckpointSnapshot> {
  return captureCheckpointSnapshot(page, actionId, "after", beforeFocusLocator);
}

function sha256Hex(bytes: Buffer): string {
  return createHash("sha256").update(bytes).digest("hex");
}

export async function captureLayoutArtifacts(
  page: Page,
  cellId: string,
): Promise<{ fullScreenshot: LayoutArtifactRef }> {
  const fullScreenshotBytes = await capturePageScreenshot(page);
  const viewport = page.viewportSize();
  return {
    fullScreenshot: {
      relativePath: `cells/${cellId}/layout-full.png`,
      contentHash: sha256Hex(fullScreenshotBytes),
      mediaType: "image/png",
      ...(viewport === null
        ? {}
        : { dimensions: { width: viewport.width, height: viewport.height } }),
    },
  };
}

export async function executeLayoutAction(
  page: Page,
  action: MatrixCell["actions"][number],
): Promise<void> {
  const timeoutMs = Number(action.params?.timeoutMs ?? "5000");
  switch (action.kind) {
    case "click":
      if (action.target === undefined || action.target.trim().length === 0) {
        throw new Error(`action ${action.id} click requires target`);
      }
      await page.click(action.target, { timeout: timeoutMs });
      return;
    case "focus":
      if (action.target === undefined || action.target.trim().length === 0) {
        throw new Error(`action ${action.id} focus requires target`);
      }
      await page.focus(action.target, { timeout: timeoutMs });
      return;
    case "wait":
      await page.waitForTimeout(Number(action.params?.ms ?? "100"));
      return;
    case "wait-selector":
      if (action.target === undefined || action.target.trim().length === 0) {
        throw new Error(`action ${action.id} wait-selector requires target`);
      }
      await page.waitForSelector(action.target, {
        timeout: timeoutMs,
        state: action.params?.state === "hidden" ? "hidden" : "visible",
      });
      return;
    default:
      throw new Error(`unsupported layout action kind: ${action.kind}`);
  }
}

export async function waitForCheckpointSettle(
  page: Page,
  action: MatrixCell["actions"][number],
): Promise<void> {
  const selector = action.params?.checkpointSettle;
  if (selector !== undefined && selector.trim().length > 0) {
    await page.waitForSelector(selector, {
      timeout: Number(action.params?.timeoutMs ?? "5000"),
      state: "visible",
    });
    return;
  }
  await page.waitForTimeout(Number(action.params?.settleMs ?? "150"));
}
