import { createHash } from "node:crypto";
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 CellEvidence,
  type ConsoleEntry,
  type NetworkEntry,
  type PageProbe,
  type RunCellInput,
  type SettleOptions,
} from "../../../playwright/src/cell-runner.js";
import { captureCellScreenshots } from "../../../playwright/src/cell-original.js";
import { captureAssetsArtifacts, captureAssetsProbe } from "./probe.js";
import type { AssetsCellEvidence } from "./types.js";
import { applyAuthAdapter } from "../cell-harness.js";

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

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 readPageProbe(page: Page): Promise<PageProbe | undefined> {
  return page.evaluate(() => {
    const globalWindow = window as typeof window & {
      __probe?: { now: number; random: number; tz: string };
    };

    const probe: PageProbe = {};
    if (globalWindow.__probe !== undefined) {
      probe.now = globalWindow.__probe.now;
      probe.random = globalWindow.__probe.random;
      probe.tz = globalWindow.__probe.tz;
    }

    return Object.keys(probe).length === 0 ? undefined : probe;
  });
}

async function waitForImageSettlement(page: Page, budgetMs: number): Promise<void> {
  await page.evaluate(async (deadlineMs) => {
    // A lazy image below the fold stays incomplete and fires neither load nor
    // error until it is scrolled into view, so waiting on the events alone
    // never returns.
    const expired = new Promise<void>((resolve) => {
      window.setTimeout(resolve, deadlineMs);
    });
    const settled = (async () => {
      const images = Array.from(document.images);
      await Promise.all(
        images.map(async (image) => {
          if (image.complete) {
            return;
          }
          await new Promise<void>((resolve) => {
            image.addEventListener("load", () => {
              resolve();
            }, { once: true });
            image.addEventListener("error", () => {
              resolve();
            }, { once: true });
          });
        }),
      );
      await Promise.all(images.map((image) => image.decode().catch(() => undefined)));
    })();
    await Promise.race([settled, expired]);
  }, budgetMs);
}

function buildAssetsEvidence(input: {
  cell: MatrixCell;
  documentDirection?: string;
  settled: CellEvidence["settled"];
  console: ConsoleEntry[];
  network: NetworkEntry[];
  pageProbe?: PageProbe;
  assetsProbe: AssetsCellEvidence["assetsProbe"];
  assetsArtifacts: AssetsCellEvidence["assetsArtifacts"];
  screenshots?: CellEvidence["screenshots"];
}): AssetsCellEvidence {
  const evidence: AssetsCellEvidence = {
    cell: input.cell,
    console: input.console,
    network: input.network,
    performance: { layoutShifts: [] },
    settled: input.settled,
    assetsProbe: input.assetsProbe,
    assetsArtifacts: input.assetsArtifacts,
  };
  if (input.documentDirection !== undefined) {
    evidence.documentDirection = input.documentDirection;
  }
  if (input.pageProbe !== undefined) {
    evidence.pageProbe = input.pageProbe;
  }
  if (input.screenshots !== undefined) {
    evidence.screenshots = input.screenshots;
  }
  return evidence;
}

export async function runAssetsCell(input: RunCellInput): Promise<AssetsCellResult> {
  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),
    });

    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);
    const documentDirection = await readDocumentDirection(page);
    await waitForImageSettlement(page, input.timeouts.settleMs);
    const [pageProbe, assetsProbe, assetsArtifacts] = await Promise.all([
      readPageProbe(page),
      captureAssetsProbe(page),
      captureAssetsArtifacts(page, input.cell.id),
    ]);

    const screenshots = await captureCellScreenshots(page, input);
    const evidence = buildAssetsEvidence({
      cell: input.cell,
      documentDirection,
      settled,
      console: buckets.console,
      network: buckets.network,
      assetsProbe,
      assetsArtifacts,
      ...(pageProbe === undefined ? {} : { pageProbe }),
      ...(screenshots === undefined ? {} : { screenshots }),
    });

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

    return {
      kind: "evidence",
      evidence,
      ...(authProof === undefined ? {} : { authProof }),
    };
  } finally {
    await context.close();
  }
}
