import crypto from "node:crypto";
import { chromium, type Browser } from "playwright-core";
import { env } from "../config/env.js";
import { storageProvider } from "../storage/index.js";

type SnapshotRequest = {
  cacheVersion: string;
  focusIndex: number | null;
  focusSelector: string | null;
  origin: string;
  projectId: string;
  sourcePath: string;
  viewportHeight: number;
  viewportWidth: number;
};

type QueueTask<T> = {
  reject: (error: unknown) => void;
  resolve: (value: T) => void;
  run: () => Promise<T>;
};

const inFlightSnapshots = new Map<string, Promise<Uint8Array>>();
const queue: QueueTask<Uint8Array>[] = [];
let activeTasks = 0;
let browserPromise: Promise<Browser> | null = null;

function encodePathname(value: string) {
  return value
    .split("/")
    .filter(Boolean)
    .map((part) => encodeURIComponent(part))
    .join("/");
}

function getSnapshotCacheKey(input: SnapshotRequest) {
  const hash = crypto
    .createHash("sha256")
    .update(
      JSON.stringify({
        focusIndex: input.focusIndex,
        focusSelector: input.focusSelector,
        sourcePath: input.sourcePath,
        viewportHeight: input.viewportHeight,
        viewportWidth: input.viewportWidth
      })
    )
    .digest("hex")
    .slice(0, 32);

  return `preview-cache/${input.projectId}/${input.cacheVersion}/${hash}.png`;
}

function buildPreviewUrl(input: SnapshotRequest) {
  const url = new URL(`/preview/projects/${encodeURIComponent(input.projectId)}/site/${encodePathname(input.sourcePath)}`, input.origin);

  if (input.focusSelector) {
    url.searchParams.set("focus", input.focusSelector);
  }

  if (input.focusIndex != null) {
    url.searchParams.set("focusIndex", String(input.focusIndex));
  }

  return url.toString();
}

async function getBrowser() {
  if (!browserPromise) {
    browserPromise = chromium.launch({
      executablePath: env.preview.chromePath,
      headless: true,
      args: ["--disable-dev-shm-usage", "--no-sandbox"]
    });
  }

  return browserPromise;
}

function pumpQueue() {
  while (activeTasks < env.preview.snapshotConcurrency && queue.length > 0) {
    const task = queue.shift();

    if (!task) {
      return;
    }

    activeTasks += 1;
    void task
      .run()
      .then(task.resolve, task.reject)
      .finally(() => {
        activeTasks -= 1;
        pumpQueue();
      });
  }
}

function enqueueSnapshot(run: () => Promise<Uint8Array>) {
  return new Promise<Uint8Array>((resolve, reject) => {
    queue.push({ reject, resolve, run });
    pumpQueue();
  });
}

async function readCachedSnapshot(key: string) {
  try {
    const cached = await storageProvider.getObject(key);

    if (cached.body.byteLength > 0) {
      return cached.body;
    }
  } catch {
    return null;
  }

  return null;
}

async function generateSnapshot(input: SnapshotRequest, cacheKey: string) {
  const browser = await getBrowser();
  const page = await browser.newPage({
    deviceScaleFactor: 1,
    viewport: {
      height: input.viewportHeight,
      width: input.viewportWidth
    }
  });

  try {
    page.setDefaultTimeout(18_000);
    await page.goto(buildPreviewUrl(input), { timeout: 25_000, waitUntil: "domcontentloaded" });
    await page.waitForLoadState("networkidle", { timeout: 8_000 }).catch(() => undefined);
    await page.waitForTimeout(env.preview.snapshotWaitMs);

    const screenshot = await page.screenshot({
      animations: "disabled",
      fullPage: false,
      type: "png"
    });
    const body = new Uint8Array(screenshot);

    await storageProvider.putObject({
      body,
      contentType: "image/png",
      key: cacheKey
    });

    return body;
  } finally {
    await page.close().catch(() => undefined);
  }
}

export async function getPreviewSnapshot(input: SnapshotRequest) {
  const cacheKey = getSnapshotCacheKey(input);
  const cached = await readCachedSnapshot(cacheKey);

  if (cached) {
    return {
      body: cached,
      cacheKey,
      status: "hit" as const
    };
  }

  const existing = inFlightSnapshots.get(cacheKey);

  if (existing) {
    return {
      body: await existing,
      cacheKey,
      status: "joined" as const
    };
  }

  const generated = enqueueSnapshot(() => generateSnapshot(input, cacheKey));
  inFlightSnapshots.set(cacheKey, generated);

  try {
    return {
      body: await generated,
      cacheKey,
      status: "miss" as const
    };
  } finally {
    inFlightSnapshots.delete(cacheKey);
  }
}
