import { readFile } from "node:fs/promises";
import { join } from "node:path";
import {
  atomicWriteBytes,
  buildArtifactRef,
  sha256Buffer,
  type ArtifactRef,
} from "../../../playwright/src/artifacts.js";
import type { Rect } from "../geometry/types.js";

export const ANNOTATED_ARTIFACT_KIND = "screenshot-annotated-derived" as const;
export const ANNOTATED_MEDIA_TYPE = "image/svg+xml" as const;

export type AnnotationMarker = {
  index: number;
  rect: Rect;
  label: string;
};

export type AnnotateScreenshotInput = {
  original: ArtifactRef;
  markers: AnnotationMarker[];
  outDir: string;
  outputRelativePath?: string;
};

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

function assertSafeRelativePath(value: string, field: string): void {
  assertNonEmptyString(value, field);
  if (
    value.startsWith("/") ||
    value.includes("\\") ||
    value.split("/").some((segment) => segment.length === 0 || segment === ".." || segment === ".")
  ) {
    throw new Error(`${field} must be a safe relative path`);
  }
}

function assertFiniteNumber(value: number, field: string): void {
  if (!Number.isFinite(value)) {
    throw new Error(`${field} must be a finite number`);
  }
}

function assertValidRect(rect: Rect, field: string): void {
  assertFiniteNumber(rect.x, `${field}.x`);
  assertFiniteNumber(rect.y, `${field}.y`);
  assertFiniteNumber(rect.width, `${field}.width`);
  assertFiniteNumber(rect.height, `${field}.height`);
  if (rect.width <= 0 || rect.height <= 0) {
    throw new Error(`${field} width and height must be positive`);
  }
}

function assertValidMarker(marker: AnnotationMarker, index: number): void {
  if (!Number.isInteger(marker.index) || marker.index < 1) {
    throw new Error(`markers[${String(index)}].index must be a positive integer`);
  }
  assertNonEmptyString(marker.label, `markers[${String(index)}].label`);
  assertValidRect(marker.rect, `markers[${String(index)}].rect`);
}

function escapeXml(value: string): string {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&apos;");
}

function readPngDimensions(bytes: Buffer): { width: number; height: number } {
  const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
  if (bytes.length < 24 || !bytes.subarray(0, 8).equals(signature)) {
    throw new Error("original artifact is not a valid PNG");
  }
  return {
    width: bytes.readUInt32BE(16),
    height: bytes.readUInt32BE(20),
  };
}

function resolveDimensions(
  original: ArtifactRef,
  bytes: Buffer,
): { width: number; height: number } {
  if (original.dimensions !== undefined) {
    const { width, height } = original.dimensions;
    if (width > 0 && height > 0) {
      return { width, height };
    }
    throw new Error("original.dimensions must contain positive width and height");
  }
  return readPngDimensions(bytes);
}

export function annotatedArtifactRelativePath(originalRelativePath: string): string {
  assertNonEmptyString(originalRelativePath, "original.relativePath");
  const slash = originalRelativePath.lastIndexOf("/");
  const directory = slash >= 0 ? originalRelativePath.slice(0, slash + 1) : "";
  const filename = slash >= 0 ? originalRelativePath.slice(slash + 1) : originalRelativePath;
  const dot = filename.lastIndexOf(".");
  const stem = dot >= 0 ? filename.slice(0, dot) : filename;
  return `${directory}${stem}-annotated.svg`;
}

function markerAnchor(rect: Rect): { x: number; y: number } {
  return {
    x: rect.x + rect.width,
    y: rect.y,
  };
}

function buildAnnotatedSvg(input: {
  width: number;
  height: number;
  markers: AnnotationMarker[];
  derivedFromHash: string;
}): string {
  const sortedMarkers = [...input.markers].sort((left, right) => left.index - right.index);

  const markerFragments = sortedMarkers
    .map((marker) => {
      const anchor = markerAnchor(marker.rect);
      const badgeRadius = 10;
      const badgeX = anchor.x + badgeRadius;
      const badgeY = anchor.y - badgeRadius;
      const safeLabel = escapeXml(marker.label);
      const safeIndex = escapeXml(String(marker.index));

      return [
        `  <g data-marker-index="${String(marker.index)}" data-geometry-source="measured">`,
        `    <rect x="${String(marker.rect.x)}" y="${String(marker.rect.y)}" width="${String(marker.rect.width)}" height="${String(marker.rect.height)}" fill="none" stroke="#e11d48" stroke-width="2" />`,
        `    <circle cx="${String(badgeX)}" cy="${String(badgeY)}" r="${String(badgeRadius)}" fill="#e11d48" />`,
        `    <text x="${String(badgeX)}" y="${String(badgeY)}" text-anchor="middle" dominant-baseline="central" fill="#ffffff" font-family="ui-sans-serif, system-ui, sans-serif" font-size="12" font-weight="700">${safeIndex}</text>`,
        `    <title>${safeLabel}</title>`,
        "  </g>",
      ].join("\n");
    })
    .join("\n");

  return [
    '<?xml version="1.0" encoding="UTF-8"?>',
    `<svg xmlns="http://www.w3.org/2000/svg" width="${String(input.width)}" height="${String(input.height)}" viewBox="0 0 ${String(input.width)} ${String(input.height)}" data-derived-from="${escapeXml(input.derivedFromHash)}" data-annotation-overlay="true" invariantum-artifact-kind="${ANNOTATED_ARTIFACT_KIND}">`,
    `  <metadata>${ANNOTATED_ARTIFACT_KIND}</metadata>`,
    markerFragments,
    "</svg>",
    "",
  ].join("\n");
}

export async function annotateScreenshot(input: AnnotateScreenshotInput): Promise<ArtifactRef> {
  assertNonEmptyString(input.outDir, "outDir");
  assertSafeRelativePath(input.original.relativePath, "original.relativePath");
  assertNonEmptyString(input.original.mediaType, "original.mediaType");
  assertNonEmptyString(input.original.contentHash, "original.contentHash");
  assertNonEmptyString(input.original.sourceRunId, "original.sourceRunId");

  if (!Array.isArray(input.markers) || input.markers.length === 0) {
    throw new Error("markers must contain at least one measured annotation");
  }

  const indices = new Set<number>();
  for (const [index, marker] of input.markers.entries()) {
    assertValidMarker(marker, index);
    if (indices.has(marker.index)) {
      throw new Error(`duplicate marker index: ${String(marker.index)}`);
    }
    indices.add(marker.index);
  }

  const originalPath = join(input.outDir, input.original.relativePath);
  const originalBytes = await readFile(originalPath);
  const actualHash = sha256Buffer(originalBytes);
  if (actualHash !== input.original.contentHash) {
    throw new Error(
      `original artifact contentHash mismatch for ${input.original.relativePath}`,
    );
  }

  const dimensions = resolveDimensions(input.original, originalBytes);
  const relativePath = input.outputRelativePath ?? annotatedArtifactRelativePath(input.original.relativePath);
  assertSafeRelativePath(relativePath, "annotation outputRelativePath");
  const svg = buildAnnotatedSvg({
    width: dimensions.width,
    height: dimensions.height,
    markers: input.markers,
    derivedFromHash: input.original.contentHash,
  });
  const svgBytes = Buffer.from(svg, "utf8");
  const absolutePath = join(input.outDir, relativePath);
  await atomicWriteBytes(absolutePath, svgBytes);

  return buildArtifactRef({
    relativePath,
    mediaType: ANNOTATED_MEDIA_TYPE,
    content: svgBytes,
    sourceRunId: input.original.sourceRunId,
    dimensions,
    redactionState: input.original.redactionState,
  });
}
