import "server-only";

import { createHmac, timingSafeEqual } from "node:crypto";
import { after } from "next/server";

import { env } from "@/lib/env";

/** Public path of an article on the website. */
export const blogPath = (slug: string) => `/blog/${slug}/`;

/** Makes root-relative upload URLs absolute, so the website can load them from this CMS. */
export function absoluteMediaUrl(url: string | null | undefined) {
  if (!url) return null;
  return url.startsWith("/") ? `${env().CMS_PUBLIC_URL}${url}` : url;
}

/** Rewrites root-relative upload URLs inside article HTML. */
export function absolutizeContentUrls(html: string) {
  const base = env().CMS_PUBLIC_URL;
  return html.replace(/(\ssrc=")(\/uploads\/)/g, `$1${base}$2`);
}

// ---------------------------------------------------------------------------
// Preview tokens
// ---------------------------------------------------------------------------

const PREVIEW_TOKEN_TTL_MS = 60 * 60 * 1000;

function sign(payload: string) {
  return createHmac("sha256", env().PREVIEW_TOKEN_SECRET)
    .update(payload)
    .digest("base64url");
}

/**
 * A signed, expiring token that lets the website render one unpublished
 * article. It carries no session and grants nothing else.
 */
export function createPreviewToken(blogId: string) {
  const payload = `${blogId}.${Date.now() + PREVIEW_TOKEN_TTL_MS}`;
  return `${Buffer.from(payload).toString("base64url")}.${sign(payload)}`;
}

export function verifyPreviewToken(token: string) {
  const [encoded, signature] = token.split(".");
  if (!encoded || !signature) return null;

  const payload = Buffer.from(encoded, "base64url").toString();
  const expected = Buffer.from(sign(payload));
  const actual = Buffer.from(signature);
  if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) {
    return null;
  }

  const [blogId, expiresAt] = payload.split(".");
  if (!blogId || !(Number(expiresAt) > Date.now())) return null;
  return { blogId };
}

export function previewUrl(blogId: string) {
  const { SITE_URL } = env();
  if (!SITE_URL) return null;
  return `${SITE_URL}/blog/preview/${createPreviewToken(blogId)}/`;
}

// ---------------------------------------------------------------------------
// Website cache revalidation
// ---------------------------------------------------------------------------

/** Cache tags the website attaches to its CMS requests. */
export const siteCacheTags = {
  posts: "cms:posts",
  post: (slug: string) => `cms:post:${slug}`,
};

/**
 * Tells the website to refetch blog content, after the response is sent so
 * editors never wait on it. Pages also revalidate on a timer, so a missed
 * call only delays an update.
 */
export function revalidateSite(slugs: (string | null | undefined)[] = []) {
  after(() => notifySite(slugs));
}

/**
 * Asks the website to refetch the blog list and the given articles, and waits
 * for the answer. Request handlers use `revalidateSite`; scripts call this.
 */
export async function notifySite(slugs: (string | null | undefined)[] = []) {
  const { SITE_URL, SITE_REVALIDATE_SECRET } = env();
  if (!SITE_URL || !SITE_REVALIDATE_SECRET) return;

  const tags = [
    siteCacheTags.posts,
    ...new Set(slugs.filter(Boolean).map((slug) => siteCacheTags.post(slug!))),
  ];

  // In batches, so a large import doesn't send one huge request.
  for (let start = 0; start < tags.length; start += 40) {
    try {
      const response = await fetch(`${SITE_URL}/api/revalidate/`, {
        method: "POST",
        headers: {
          "content-type": "application/json",
          authorization: `Bearer ${SITE_REVALIDATE_SECRET}`,
        },
        body: JSON.stringify({ tags: tags.slice(start, start + 40) }),
        signal: AbortSignal.timeout(5000),
      });
      if (!response.ok) {
        console.warn(`[revalidate] website responded ${response.status}`);
      }
    } catch (error) {
      console.warn("[revalidate] website unreachable", error);
    }
  }
}
