import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import type { Page } from "playwright";
import type { HarnessEvent } from "../../../core/src/classify/classifier.js";
import { matrixCellRef } from "../../../schema/src/records/context.js";
import type { MatrixCell } from "../../../schema/src/records/context.js";
import type { AuthProof } from "../../../playwright/src/auth.js";
import {
  awaitSettled,
  readDocumentDirection,
  type CaptureOptions,
  type ConsoleEntry,
  type NetworkEntry,
  type RunCellInput,
  type SettleOptions,
} from "../../../playwright/src/cell-runner.js";
import { buildSettleInitScript } from "../../../playwright/src/settled.js";
import { captureCellOriginal } from "../../../playwright/src/cell-original.js";
import { contentAddressedFileName } from "../../../playwright/src/artifacts.js";
import { applyAuthAdapter } from "../cell-harness.js";
import { captureMeasuredLayoutProbe } from "./probe.js";
import type {
  MeasuredLayoutArtifactRef,
  MeasuredLayoutCellEvidence,
  MeasuredLayoutProbe,
} from "./types.js";

export type MeasuredLayoutCellResult =
  | { kind: "evidence"; evidence: MeasuredLayoutCellEvidence; authProof?: AuthProof }
  | {
      kind: "harness";
      outcome: HarnessEvent;
      partialEvidence?: MeasuredLayoutCellEvidence;
    };

function seedToUint32(seed: string): number {
  const digest = createHash("sha256").update(seed).digest();
  return digest.readUInt32LE(0);
}

function buildDeterminismInitScript(clockStartMs: number, seed: string): string {
  const seedUint = seedToUint32(seed);
  return `(() => {
    const CLOCK_START_MS = ${String(clockStartMs)};
    const SEED = ${String(seedUint)};
    let tick = 0;
    const OriginalDate = Date;
    class PinnedDate extends OriginalDate {
      constructor(...args) {
        if (args.length === 0) {
          super(CLOCK_START_MS + tick);
          tick += 1;
          return;
        }
        super(...args);
      }
      static now() {
        const value = CLOCK_START_MS + tick;
        tick += 1;
        return value;
      }
    }
    PinnedDate.parse = OriginalDate.parse;
    PinnedDate.UTC = OriginalDate.UTC;
    PinnedDate.prototype = OriginalDate.prototype;
    Object.setPrototypeOf(PinnedDate, OriginalDate);
    globalThis.Date = PinnedDate;
    let state = SEED >>> 0;
    const nextRandom = () => {
      state = (state + 0x6d2b79f5) >>> 0;
      let t = Math.imul(state ^ (state >>> 15), state | 1);
      t = (t + Math.imul(t ^ (t >>> 7), t | 61)) ^ t;
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
    Math.random = nextRandom;
  })();`;
}

function resolveTimezone(cell: MatrixCell): string {
  const fromState = cell.state.timezone;
  if (fromState !== undefined && fromState.trim().length > 0) {
    return fromState;
  }
  return "UTC";
}

function joinUrl(baseUrl: string, path: string): string {
  if (path.startsWith("http://") || path.startsWith("https://")) {
    return path;
  }
  const normalizedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
  const normalizedPath = path.startsWith("/") ? path : `/${path}`;
  return `${normalizedBase}${normalizedPath}`;
}

function harnessOutcome(input: {
  phase: HarnessEvent["phase"];
  cell: MatrixCell;
  code: string;
  message: string;
  retryable: boolean;
}): HarnessEvent {
  return {
    phase: input.phase,
    scope: { id: input.cell.id },
    plannedContext: {
      kind: "browser",
      cell: matrixCellRef(input.cell),
    },
    cause: {
      code: input.code,
      message: input.message,
      retryable: input.retryable,
    },
    artifactRefs: [],
  };
}

function attachCaptureListeners(
  page: Page,
  capture: CaptureOptions,
  buckets: { console: ConsoleEntry[]; network: NetworkEntry[] },
): void {
  if (capture.console) {
    page.on("console", (message) => {
      const location = message.location();
      buckets.console.push({
        type: message.type(),
        text: message.text(),
        location: {
          url: location.url,
          lineNumber: location.lineNumber,
          columnNumber: location.columnNumber,
        },
      });
    });
    page.on("pageerror", (error) => {
      buckets.console.push({
        type: "error",
        text: error.message,
      });
    });
  }

  if (capture.network) {
    page.on("response", (response) => {
      const request = response.request();
      buckets.network.push({
        url: request.url(),
        method: request.method(),
        status: response.status(),
        resourceType: request.resourceType(),
      });
    });
  }
}

async function captureMeasuredLayoutArtifacts(
  page: Page,
  cellId: string,
  options: {
    artifactRunDir?: string;
    sourceRunId?: string;
    redactionRules?: RunCellInput["redactionRules"];
  },
): Promise<{ fullScreenshot: MeasuredLayoutArtifactRef }> {
  const screenshot = await captureCellOriginal(page, cellId, {
    fileName: "measured-layout-full.png",
    ...(options.artifactRunDir === undefined ? {} : { artifactRunDir: options.artifactRunDir }),
    ...(options.sourceRunId === undefined ? {} : { sourceRunId: options.sourceRunId }),
    ...(options.redactionRules === undefined ? {} : { redactionRules: options.redactionRules }),
  });
  const fullScreenshot: MeasuredLayoutArtifactRef = {
    ...screenshot.ref,
    ...(options.sourceRunId === undefined ? {} : { sourceRunId: options.sourceRunId }),
  };
  await assertImmutableMeasuredLayoutScreenshot(
    fullScreenshot,
    cellId,
    options.artifactRunDir,
  );
  return {
    fullScreenshot,
  };
}

export async function assertImmutableMeasuredLayoutScreenshot(
  screenshot: MeasuredLayoutArtifactRef,
  cellId: string,
  artifactRunDir?: string,
): Promise<void> {
  const expectedPath = `cells/${cellId}/${contentAddressedFileName(
    "measured-layout-full.png",
    screenshot.contentHash,
  )}`;
  if (
    !/^[a-f0-9]{64}$/.test(screenshot.contentHash)
    || screenshot.relativePath !== expectedPath
    || screenshot.mediaType !== "image/png"
  ) {
    throw new Error("measured layout original screenshot identity is corrupt");
  }
  if (artifactRunDir === undefined) return;
  const bytes = await readFile(join(artifactRunDir, screenshot.relativePath));
  const contentHash = createHash("sha256").update(bytes).digest("hex");
  if (contentHash !== screenshot.contentHash) {
    throw new Error("measured layout original screenshot bytes changed after capture");
  }
}

function buildMeasuredLayoutEvidence(input: {
  cell: MatrixCell;
  documentDirection?: string;
  settled: MeasuredLayoutCellEvidence["settled"];
  console: ConsoleEntry[];
  network: NetworkEntry[];
  measuredLayoutProbe: MeasuredLayoutProbe;
  measuredLayoutArtifacts: MeasuredLayoutCellEvidence["measuredLayoutArtifacts"];
}): MeasuredLayoutCellEvidence {
  const evidence: MeasuredLayoutCellEvidence = {
    cell: input.cell,
    console: input.console,
    network: input.network,
    performance: { layoutShifts: [] },
    settled: input.settled,
    measuredLayoutProbe: input.measuredLayoutProbe,
    measuredLayoutArtifacts: input.measuredLayoutArtifacts,
  };
  if (input.documentDirection !== undefined) {
    evidence.documentDirection = input.documentDirection;
  }
  return evidence;
}

export async function runMeasuredLayoutCell(
  input: RunCellInput,
): Promise<MeasuredLayoutCellResult> {
  const clockStartMs = Date.parse(input.clockStart);
  const timezoneId = resolveTimezone(input.cell);
  const buckets = { console: [] as ConsoleEntry[], network: [] as NetworkEntry[] };

  const context = await input.browser.newContext({
    locale: input.cell.locale,
    viewport: {
      width: input.cell.viewport.width,
      height: input.cell.viewport.height,
    },
    deviceScaleFactor: input.cell.viewport.deviceScaleFactor,
    timezoneId,
  });

  try {
    const authResult = await applyAuthAdapter({
      adapter: input.authAdapter,
      context,
      cell: input.cell,
    });
    if (authResult.kind === "harness") {
      return {
        kind: "harness",
        outcome: harnessOutcome({ phase: "adapter", cell: input.cell, ...authResult }),
      };
    }
    const authProof = authResult.proof;

    await context.addInitScript({
      content: `${buildDeterminismInitScript(clockStartMs, input.seed)}\n${buildSettleInitScript()}`,
    });

    const page = await context.newPage();
    attachCaptureListeners(page, input.capture, buckets);

    const targetUrl = joinUrl(input.baseUrl, input.cell.url);
    try {
      await page.goto(targetUrl, {
        timeout: input.timeouts.navigateMs,
        waitUntil: "domcontentloaded",
      });
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      return {
        kind: "harness",
        outcome: harnessOutcome({
          phase: "setup",
          cell: input.cell,
          code: "navigation-failed",
          message,
          retryable: true,
        }),
      };
    }

    const settleOptions: SettleOptions = {
      timeoutMs: input.timeouts.settleMs,
      ...input.capture.settle,
    };
    const settled = await awaitSettled(page, settleOptions);
    try {
      const documentDirection = await readDocumentDirection(page);
      await page.waitForTimeout(100);
      const measuredLayoutProbe = await captureMeasuredLayoutProbe(page, input.cell, settleOptions);
      const measuredLayoutArtifacts = await captureMeasuredLayoutArtifacts(page, input.cell.id, {
        ...(input.artifactRunDir === undefined ? {} : { artifactRunDir: input.artifactRunDir }),
        ...(input.sourceRunId === undefined ? {} : { sourceRunId: input.sourceRunId }),
        ...(input.redactionRules === undefined ? {} : { redactionRules: input.redactionRules }),
      });
      const evidence = buildMeasuredLayoutEvidence({
        cell: input.cell,
        documentDirection,
        settled,
        console: buckets.console,
        network: buckets.network,
        measuredLayoutProbe,
        measuredLayoutArtifacts,
      });

      if (settled.timedOut || measuredLayoutProbe.settledState.timedOut) {
        return {
          kind: "harness",
          outcome: harnessOutcome({
            phase: "infrastructure",
            cell: input.cell,
            code: "settle-timeout",
            message: "Page did not reach settled state before measured layout capture",
            retryable: false,
          }),
          partialEvidence: evidence,
        };
      }

      return {
        kind: "evidence",
        evidence,
        ...(authProof === undefined ? {} : { authProof }),
      };
    } catch (error) {
      const message = error instanceof Error ? error.message : String(error);
      return {
        kind: "harness",
        outcome: harnessOutcome({
          phase: "infrastructure",
          cell: input.cell,
          code: "measured-layout-capture-failed",
          message,
          retryable: false,
        }),
      };
    }
  } finally {
    await context.close();
  }
}
