import type { Page } from "playwright";
import type { MatrixCell } from "../../schema/src/records/context.js";
import {
  persistArtifact,
  type ArtifactPayload,
  type ArtifactRef,
} from "./artifacts.js";
import {
  layoutShiftObserverScript,
  type LayoutShiftEntry,
} from "./layout-shift-observer.js";
import {
  redactEvidencePayload,
  redactionStateForArtifact,
  type RedactionRule,
} from "./redaction.js";
import type { SettledReport } from "./settled.js";
import { capturePageScreenshotWithDimensions } from "./screenshot.js";

export type { ArtifactRef } from "./artifacts.js";

export type ConsoleEntry = {
  type: string;
  text: string;
  location?: { url?: string; lineNumber?: number; columnNumber?: number };
  stack?: string;
};

export type NetworkEntry = {
  url: string;
  method: string;
  status?: number;
  resourceType: string;
  requestHeaders?: Record<string, string>;
  responseHeaders?: Record<string, string>;
  requestBody?: string;
  responseBody?: string;
};

export type { LayoutShiftEntry } from "./layout-shift-observer.js";

export type CellEvidence = {
  cell: MatrixCell;
  console: ConsoleEntry[];
  network: NetworkEntry[];
  domSnapshot: ArtifactRef;
  performance: { layoutShifts: LayoutShiftEntry[] };
  screenshots: { original: ArtifactRef };
  trace?: ArtifactRef;
  settled: SettledReport;
};

export type CellEvidencePayload = {
  cell: MatrixCell;
  console: ConsoleEntry[];
  network: NetworkEntry[];
  domSnapshot: ArtifactPayload;
  performance: { layoutShifts: LayoutShiftEntry[] };
  screenshots: { original: ArtifactPayload };
  trace?: ArtifactPayload;
  settled: SettledReport;
};

export type CaptureBuckets = {
  console: ConsoleEntry[];
  network: NetworkEntry[];
};

export type EvidenceCaptureOptions = {
  console: boolean;
  network: boolean;
};

export type PageArtifactCaptureOptions = {
  domSnapshot: boolean;
  screenshot: boolean;
  performance: boolean;
  trace: boolean;
};

export type CapturedPageArtifacts = {
  domSnapshot: ArtifactPayload;
  screenshotOriginal: ArtifactPayload;
  layoutShifts: LayoutShiftEntry[];
  trace?: ArtifactPayload;
};

function assertNonEmptyString(value: string, field: string): void {
  if (value.trim().length === 0) {
    throw new Error(`${field} must be a non-empty string`);
  }
}

function normalizeHeaderRecord(
  headers: Record<string, string> | undefined,
): Record<string, string> | undefined {
  if (headers === undefined) {
    return undefined;
  }
  const normalized: Record<string, string> = {};
  for (const [key, value] of Object.entries(headers)) {
    normalized[key.toLowerCase()] = value;
  }
  return normalized;
}

function resolveSourceRunId(cell: MatrixCell): string {
  const runId = cell.creationSource.runId.trim();
  if (runId.length === 0) {
    throw new Error("cell.creationSource.runId must be a non-empty string");
  }
  return runId;
}

export function attachEvidenceCapture(
  page: Page,
  buckets: CaptureBuckets,
  options: EvidenceCaptureOptions,
): void {
  if (options.console) {
    page.on("console", (message) => {
      const location = message.location();
      const entry: ConsoleEntry = {
        type: message.type(),
        text: message.text(),
        location: {
          url: location.url,
          lineNumber: location.lineNumber,
          columnNumber: location.columnNumber,
        },
      };
      if (message.type() === "error") {
        const args = message.args();
        void Promise.all(args.map((arg) => arg.jsonValue().catch(() => undefined)))
          .then((values) => {
            const stackCandidate = values.find(
              (value) =>
                typeof value === "object" &&
                value !== null &&
                "stack" in value &&
                typeof (value as { stack?: unknown }).stack === "string",
            ) as { stack: string } | undefined;
            if (stackCandidate !== undefined) {
              entry.stack = stackCandidate.stack;
            }
          })
          .catch(() => undefined);
      }
      buckets.console.push(entry);
    });

    page.on("pageerror", (error) => {
      const entry: ConsoleEntry = {
        type: "error",
        text: error.message,
      };
      if (error.stack !== undefined) {
        entry.stack = error.stack;
      }
      buckets.console.push(entry);
    });
  }

  if (options.network) {
    page.on("response", (response) => {
      const request = response.request();
      const entry: NetworkEntry = {
        url: request.url(),
        method: request.method(),
        status: response.status(),
        resourceType: request.resourceType(),
      };
      const requestHeaders = normalizeHeaderRecord(request.headers());
      const responseHeaders = normalizeHeaderRecord(response.headers());
      if (requestHeaders !== undefined) {
        entry.requestHeaders = requestHeaders;
      }
      if (responseHeaders !== undefined) {
        entry.responseHeaders = responseHeaders;
      }
      buckets.network.push(entry);
    });
  }
}

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

async function installLayoutShiftObserver(page: Page): Promise<void> {
  await page.evaluate(layoutShiftObserverScript());
}

export async function capturePageArtifacts(
  page: Page,
  options: PageArtifactCaptureOptions,
): Promise<CapturedPageArtifacts> {
  if (options.performance) {
    await installLayoutShiftObserver(page);
  }

  const domSnapshot = options.domSnapshot
    ? {
        bytes: Buffer.from(await page.content(), "utf8"),
        mediaType: "text/html",
      }
    : {
        bytes: Buffer.from("", "utf8"),
        mediaType: "text/html",
      };

  const screenshot = options.screenshot ? await capturePageScreenshotWithDimensions(page) : undefined;
  const screenshotOriginal: ArtifactPayload = {
    bytes: screenshot?.bytes ?? Buffer.from(""),
    mediaType: "image/png",
    ...(screenshot === undefined ? {} : { dimensions: screenshot.dimensions }),
  };

  const layoutShifts = options.performance ? await readLayoutShifts(page) : [];

  return {
    domSnapshot,
    screenshotOriginal,
    layoutShifts,
  };
}

export function finalizeCapturedEvidence(input: {
  cell: MatrixCell;
  buckets: CaptureBuckets;
  settled: SettledReport;
  artifacts: CapturedPageArtifacts;
}): CellEvidencePayload {
  const payload: CellEvidencePayload = {
    cell: input.cell,
    console: input.buckets.console,
    network: input.buckets.network,
    domSnapshot: input.artifacts.domSnapshot,
    performance: { layoutShifts: input.artifacts.layoutShifts },
    screenshots: { original: input.artifacts.screenshotOriginal },
    settled: input.settled,
  };
  if (input.artifacts.trace !== undefined) {
    payload.trace = input.artifacts.trace;
  }
  return payload;
}

export async function writeCellArtifacts(
  runDir: string,
  cellId: string,
  evidence: CellEvidencePayload,
  rules: RedactionRule[],
): Promise<CellEvidence> {
  assertNonEmptyString(runDir, "runDir");
  assertNonEmptyString(cellId, "cellId");

  if (evidence.cell.id !== cellId) {
    throw new Error("cellId must match evidence.cell.id");
  }

  const sourceRunId = resolveSourceRunId(evidence.cell);
  const { payload: redactedEvidence, artifactReplacements } = redactEvidencePayload(
    evidence,
    rules,
  );

  const domSnapshot = await persistArtifact({
    runDir,
    cellId,
    kind: "dom-snapshot",
    payload: redactedEvidence.domSnapshot,
    sourceRunId,
    redactionState: redactionStateForArtifact(artifactReplacements.domSnapshot),
  });
  const screenshotOriginal = await persistArtifact({
    runDir,
    cellId,
    kind: "screenshot-original",
    payload: redactedEvidence.screenshots.original,
    sourceRunId,
    redactionState: "none",
  });

  const persisted: CellEvidence = {
    cell: redactedEvidence.cell,
    console: redactedEvidence.console,
    network: redactedEvidence.network,
    domSnapshot,
    performance: redactedEvidence.performance,
    screenshots: { original: screenshotOriginal },
    settled: redactedEvidence.settled,
  };

  if (redactedEvidence.trace !== undefined) {
    persisted.trace = await persistArtifact({
      runDir,
      cellId,
      kind: "trace",
      payload: redactedEvidence.trace,
      sourceRunId,
      redactionState: redactionStateForArtifact(artifactReplacements.trace),
    });
  }

  return persisted;
}
