import { revalidateTag } from "next/cache";
import { Buffer } from "node:buffer";
import crypto from "node:crypto";
import { receiveV2Notification } from "./delivery-notification-v2";
import { SMKING_SITEMAP_CACHE_TAG } from "./sitemap";

interface WebhookPayload {
  kind?: string;
  paths?: string[];
  slugs?: string[];
  deliveredAt?: string;
  deliveryId?: string;
}

// Replay protection (v0.21.1+). The deliveredAt window is the primary
// defense — ±5min absorbs clock skew between SaaS and customer while
// capping how long an intercepted delivery stays replayable. deliveryId
// dedup is best-effort on top: serverless instances each hold their own
// Map, so a replay landing on a different instance only has the window
// to beat. SaaS payloads predating deliveryId pass dedup untouched.
const REPLAY_WINDOW_MS = 5 * 60 * 1000;
const SEEN_DELIVERIES_MAX = 1000;
const seenDeliveries = new Map<string, number>(); // deliveryId → expiry (ms)

function isDuplicateDelivery(
  deliveryId: string | undefined,
  now: number,
): boolean {
  for (const [id, expiry] of seenDeliveries) {
    if (expiry <= now) seenDeliveries.delete(id);
  }
  if (typeof deliveryId !== "string" || deliveryId.length === 0) return false;
  if (seenDeliveries.has(deliveryId)) return true;
  if (seenDeliveries.size >= SEEN_DELIVERIES_MAX) {
    // Map iterates in insertion order — evict the oldest entry.
    const oldest = seenDeliveries.keys().next().value;
    if (oldest !== undefined) seenDeliveries.delete(oldest);
  }
  seenDeliveries.set(deliveryId, now + REPLAY_WINDOW_MS);
  return false;
}

/**
 * SmKing unified webhook handler (v0.11+).
 *
 * Drop-in install:
 *
 * ```ts
 * // app/api/smking/webhook/route.ts
 * export { POST } from "@soloworks/smking-next/webhook";
 * ```
 *
 * Replaces v0.10's split handlers:
 *   - `/route`        AEO, Bearer-authed (SMKING_WEBHOOK_TOKEN) — GONE
 *   - `/cms-webhook`  CMS, HMAC-signed (SMKING_WEBHOOK_SECRET) — GONE
 *
 * Single HMAC-SHA256 endpoint. Payload `.kind` (`"aeo" | "cms_page" |
 * future kinds`) dispatches which revalidate tag namespace to use:
 *
 *   smking:{kind}:{identifier}
 *
 * Where identifier is either:
 *   - path  (AEO: `["/products/foo"]`) — `smking:aeo:/products/foo`
 *   - slug  (CMS: `["hello"]`) — `smking:cms_page:hello`
 *
 * Customer migration from v0.10:
 *   1. env: SMKING_WEBHOOK_TOKEN → SMKING_WEBHOOK_SECRET (HMAC key)
 *   2. route path: /api/smking-revalidate + /api/smking/webhook
 *      → /api/smking/webhook (single)
 *   3. revalidateTag callers update tag prefix
 *
 * Auth: HMAC-SHA256 only. Bearer dropped — single auth model means one
 * env var, one shim, fewer customer mis-config paths (the disambiguation
 * problem the v0.10 dual-handler shipped with).
 *
 * Replay protection (v0.21.1+): a signed delivery is only accepted while
 * `deliveredAt` sits inside a ±5min window (401 `stale_delivery`
 * otherwise — missing field counts as stale), and a `deliveryId` seen
 * before within that window is rejected (401 `duplicate_delivery`).
 * Payloads from a SaaS predating `deliveryId` skip dedup — the window
 * remains the primary defense.
 */
export async function POST(request: Request): Promise<Response> {
  const secret = process.env.SMKING_WEBHOOK_SECRET;
  if (!secret) {
    return Response.json(
      { error: "webhook_secret_missing" },
      { status: 503 },
    );
  }

  // Raw body for HMAC verify — re-encoding via JSON.parse + stringify
  // would change byte order / spacing and invalidate the signature.
  const rawBody = await request.text();
  const providedSig = request.headers.get("x-smking-signature");
  if (!verifySignature(rawBody, providedSig, secret)) {
    return Response.json({ error: "invalid_signature" }, { status: 401 });
  }

  let payload: WebhookPayload;
  try {
    payload = JSON.parse(rawBody) as WebhookPayload;
  } catch {
    return Response.json({ error: "invalid_payload" }, { status: 400 });
  }

  // Replay protection — deliveredAt must sit inside a ±5min window
  // (missing/unparseable counts as stale; SaaS has sent it since v0.11),
  // then deliveryId dedup rejects re-sends of a delivery we already saw.
  const now = Date.now();
  const deliveredAtMs = payload.deliveredAt
    ? Date.parse(payload.deliveredAt)
    : Number.NaN;
  if (
    Number.isNaN(deliveredAtMs) ||
    Math.abs(now - deliveredAtMs) > REPLAY_WINDOW_MS
  ) {
    return Response.json({ error: "stale_delivery" }, { status: 401 });
  }
  if (isDuplicateDelivery(payload.deliveryId, now)) {
    return Response.json({ error: "duplicate_delivery" }, { status: 401 });
  }

  if (payload.kind === "content_delivery_probe_v2" || payload.kind === "content_delivery_v2") {
    return receiveV2Notification(payload, Buffer.byteLength(rawBody));
  }

  const kind = payload.kind;
  if (typeof kind !== "string" || kind.length === 0) {
    // Forward-compat: SaaS may emit kinds we haven't taught the SDK
    // yet — acknowledge (200) without action so SaaS doesn't retry.
    return Response.json({ ok: true, note: "no_action_taken" });
  }

  let revalidated = 0;
  let errors = 0;
  for (const path of payload.paths ?? []) {
    try {
      revalidateTag(`smking:${kind}:${path}`, "default");
      revalidated++;
    } catch (err) {
      errors++;
      console.warn(
        `[@soloworks/smking-next/webhook] revalidateTag failed for ${kind}/${path}:`,
        err,
      );
    }
  }
  for (const slug of payload.slugs ?? []) {
    try {
      revalidateTag(`smking:${kind}:${slug}`, "default");
      revalidated++;
    } catch (err) {
      errors++;
      console.warn(
        `[@soloworks/smking-next/webhook] revalidateTag failed for ${kind}/${slug}:`,
        err,
      );
    }
  }

  // The canonical sitemap fetch has a daily TTL. A publish must evict it
  // immediately or newly published CMS pages can stay absent for 24 hours.
  // Limit this shared tag to content kinds that actually affect the sitemap;
  // unknown future payloads keep their forward-compatible no-op behavior.
  if (kind === "aeo" || kind === "cms_page") {
    try {
      revalidateTag(SMKING_SITEMAP_CACHE_TAG, "default");
      revalidated++;
    } catch (err) {
      errors++;
      console.warn(
        "[@soloworks/smking-next/webhook] sitemap revalidateTag failed:",
        err,
      );
    }
  }

  return Response.json({ ok: true, kind, revalidated, errors });
}

function verifySignature(
  rawBody: string,
  signatureHeader: string | null,
  secret: string,
): boolean {
  if (!signatureHeader || !signatureHeader.startsWith("sha256=")) return false;
  const provided = signatureHeader.slice("sha256=".length);
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  if (expected.length !== provided.length) return false;
  try {
    return crypto.timingSafeEqual(
      Buffer.from(expected, "hex"),
      Buffer.from(provided, "hex"),
    );
  } catch {
    return false;
  }
}
