import { NextResponse } from "next/server";
import { revalidateTag } from "next/cache";
import { createHash, timingSafeEqual } from "node:crypto";
import {
  ALL_CONTENT_TAG,
  franchiseeTag,
  franchiseeTypeTag,
} from "@/src/lib/cache/tenantCache";

/**
 * Cache purge endpoint, called by the content backend when a franchisee
 * publishes.
 *
 * The TTL in tenantCache is the backstop; this is the optimisation that makes
 * an edit appear immediately instead of within five minutes. Everything here is
 * therefore best-effort by design: a purge that never arrives costs staleness,
 * not correctness.
 *
 * The backend sends WHAT CHANGED ({ franchiseeId, contentType }), not tag
 * strings. That keeps the tag vocabulary owned by this repo - changing the
 * scheme is a deploy here, not a coordinated backend release.
 *
 * Each PM2 deployment (production 3003, preview 3002, staging 3007) holds its
 * own in-process cache, so the backend must call all three.
 */
export const dynamic = "force-dynamic";

interface PurgeRequest {
  /** Null for global/corporate content. */
  franchiseeId?: string | null;
  /** e.g. "pages" | "programs" | "activities" | "header-footer". */
  contentType?: string | null;
}

/**
 * Hash both sides first so `timingSafeEqual` always receives equal-length
 * buffers - it throws on a length mismatch, which would itself leak the secret
 * length.
 */
function secretMatches(given: string, expected: string): boolean {
  if (!expected) return false;
  const a = createHash("sha256").update(given).digest();
  const b = createHash("sha256").update(expected).digest();
  return timingSafeEqual(a, b);
}

export async function POST(request: Request) {
  const expected = process.env.REVALIDATE_SECRET ?? "";
  const given = request.headers.get("x-revalidate-secret") ?? "";

  // An unauthenticated purge route is a free DoS amplifier: anyone could loop
  // it and force the backend to re-serve ~96 KB of nav data indefinitely.
  if (!secretMatches(given, expected)) {
    console.warn("[revalidate] rejected: bad or missing secret");
    return NextResponse.json({ ok: false }, { status: 401 });
  }

  let body: PurgeRequest = {};
  try {
    body = (await request.json()) as PurgeRequest;
  } catch {
    // An empty/!JSON body is treated as "purge global", which is the safe
    // direction to fail in - broader than needed, never narrower.
  }

  const franchiseeId = body.franchiseeId ?? null;
  const contentType = body.contentType ?? null;

  // Narrowest tag that covers the change:
  //
  //   tenant + type  - one franchisee edited one kind of content
  //   tenant         - one franchisee edited something unspecified
  //   content:all    - global content changed. Tenants without a row of their
  //                    own inherit it, so their cached copy may be this very
  //                    content and everything has to go. Rare, and the only
  //                    case where a single edit legitimately flushes all
  //                    tenants.
  const tag = franchiseeId
    ? contentType
      ? franchiseeTypeTag(franchiseeId, contentType)
      : franchiseeTag(franchiseeId)
    : ALL_CONTENT_TAG;

  // Next 16 changed the signature to revalidateTag(tag, profile). `expire: 0`
  // is the "treat as expired right now" profile - a purge, which is what a
  // publish webhook means. Next 15's single-argument form no longer typechecks.
  revalidateTag(tag, { expire: 0 });

  // Logged so "my edit isn't showing" can be answered in order: did the backend
  // send it, did this deployment get a 200, is it past the TTL anyway.
  //
  // console.warn, not console.log: next.config.ts strips console.log from
  // production bundles (compiler.removeConsole excludes only error and warn),
  // so a .log here is present in dev and silently gone exactly where this
  // record matters. Verified by running a build - the rejection warning below
  // survived and this line did not.
  console.warn(`[revalidate] purged ${tag}`);

  return NextResponse.json({ ok: true, purged: [tag] });
}
