import type { Metadata } from "next";
import { type Locale } from "next-intl";
import { getTranslations } from "next-intl/server";
import { getPathname } from "@/i18n/navigation";
import { routing } from "@/i18n/routing";
import { site } from "../site";
import { robotsMetaContent } from "./robots";
import { aiMetaTags } from "./ai";

export type Keywords = readonly [string, ...string[]];

export const seo = {
  titleTemplate: `%s | ${site.titleSuffix}`,
  defaultTitle: site.titleSuffix,
  openGraph: {
    type: "website" as const,
    siteName: site.name,
  },
  twitter: {
    card: "summary_large_image" as const,
    site: site.social.twitter,
  },
  verification: {
    google: process.env.NEXT_PUBLIC_GOOGLE_VERIFICATION,
    bing: process.env.NEXT_PUBLIC_BING_VERIFICATION,
  },
  sitemap: {
    exclude: [] as string[],
    changeFreq: "weekly" as const,
    priority: 0.7,
  },
};

const OG_LOCALES: Record<Locale, string> = {
  en: "en_US",
  fr: "fr_FR",
};

export function ogLocaleFor(locale: Locale): string {
  return OG_LOCALES[locale];
}

export function ogAlternateLocalesFor(locale: Locale): string[] {
  return routing.locales.filter((l) => l !== locale).map((l) => OG_LOCALES[l]);
}

/**
 * The social card rendered by `src/app/[locale]/opengraph-image.tsx`.
 *
 * Declared here because both ends need the same numbers: the route exports them
 * as `size` / `contentType`, and the page metadata restates them as
 * `og:image:width` / `og:image:height` / `og:image:type`.
 */
export const OG_IMAGE = {
  size: { width: 1200, height: 630 },
  contentType: "image/png",
  /**
   * Cache-busting stamp appended to the card URL as `?v=`.
   *
   * Social platforms cache a scraped `og:image` by URL, sometimes for weeks, and
   * the route path never changes. **Bump this date whenever the card's artwork,
   * copy or product shot changes**, otherwise the old card keeps being served.
   */
  version: "2026-08-25",
} as const;

/**
 * The `openGraph.images` entry for a locale, with its localised alt.
 *
 * `opengraph-image.tsx` can only export a single module-level `alt`, shared by
 * every `[locale]`. Declaring the image from the page instead is what buys the
 * per-locale `og:image:alt` — and it keeps the route prerendered, which
 * `generateImageMetadata` would not (see the route's own comment).
 *
 * The URL therefore points at the route by hand; `metadataBase` (set on the
 * layout) turns it into an absolute URL. `twitter:image` is not declared: when
 * `twitter` has no own `images`, `postProcessMetadata` copies `openGraph.images`
 * onto it, alt included.
 *
 * The prefix comes from `getPathname` — the same source as the canonical and the
 * hreflang alternates — so it follows `localePrefix: "as-needed"`: hardcoding
 * `/${locale}` made every English card resolve to `/en/opengraph-image`, which
 * the proxy answers with a 307 to `/opengraph-image`. Scrapers that do not
 * follow redirects (several messaging previewers and validators) then show no
 * card at all.
 */
export async function ogImagesFor(locale: Locale) {
  const t = await getTranslations({ locale, namespace: "OpenGraph" });
  const localeRoot = getPathname({ href: "/", locale });
  const prefix = localeRoot === "/" ? "" : localeRoot;
  return [
    {
      url: `${prefix}/opengraph-image?v=${OG_IMAGE.version}`,
      width: OG_IMAGE.size.width,
      height: OG_IMAGE.size.height,
      type: OG_IMAGE.contentType,
      alt: t("imageAlt"),
    },
  ];
}

export type Href = Parameters<typeof getPathname>[0]["href"];

/**
 * Absolute, locale-prefixed URL for a typed route.
 *
 * Same `getPathname` source as `buildPageAlternates`, so a JSON-LD `url` and the
 * `<link rel="canonical">` of the same page can never disagree — `metadataBase`
 * resolves the relative canonical against `site.url`, which is what we prepend here.
 */
export function absolutePageUrl({ href, locale }: { href: Href; locale: Locale }): string {
  return `${site.url}${getPathname({ href, locale })}`;
}

function buildPageAlternates({ href, locale }: { href: Href; locale: Locale }) {
  const canonical = getPathname({ href, locale });
  const languages = Object.fromEntries(
    routing.locales.map((alt) => [alt, getPathname({ href, locale: alt })]),
  );
  return {
    canonical,
    languages: {
      ...languages,
      "x-default": getPathname({ href, locale: routing.defaultLocale }),
    },
  } satisfies NonNullable<Metadata["alternates"]>;
}

type RawReader = { raw: (key: string) => unknown };

function assertKeywords(value: unknown, where: string): Keywords {
  if (
    !Array.isArray(value) ||
    value.length === 0 ||
    !value.every((v): v is string => typeof v === "string")
  ) {
    throw new Error(
      `[seo] Missing or empty metaKeywords for "${where}". Each namespace must declare a non-empty string[] under "metaKeywords".`,
    );
  }
  return value as unknown as Keywords;
}

export function readKeywords(t: unknown, where: string): Keywords {
  return assertKeywords((t as RawReader).raw("metaKeywords"), where);
}

export async function getLocaleKeywords(locale: Locale): Promise<Keywords> {
  const t = await getTranslations({ locale, namespace: "LocaleLayout" });
  return readKeywords(t, "LocaleLayout");
}

export async function buildPageMetadata({
  href,
  locale,
  title,
  description,
  keywords,
}: {
  href: Href;
  locale: Locale;
  title: string;
  description: string;
  keywords?: Keywords;
}): Promise<Metadata> {
  const globalKeywords = await getLocaleKeywords(locale);
  const merged = Array.from(new Set([...(keywords ?? []), ...globalKeywords]));
  const alternates = buildPageAlternates({ href, locale });
  return {
    title,
    description,
    keywords: merged,
    alternates,
    authors: [{ name: site.name }],
    publisher: site.name,
    // Metadata objects from nested segments are merged SHALLOWLY: an `openGraph`
    // (or `twitter`) key here replaces the layout's wholesale, it is not merged
    // into it. So every field the layout declares has to be restated, otherwise
    // `og:type`, `og:site_name` and `twitter:card` silently disappear from every
    // page built through this helper.
    // `images` is declared rather than left to Next: the file-based
    // `opengraph-image.tsx` is only injected when the segment's `openGraph` has
    // no own `images` key, and its `alt` cannot be localised. Declaring the same
    // card here carries this locale's `og:image:alt`.
    openGraph: {
      type: seo.openGraph.type,
      siteName: seo.openGraph.siteName,
      title,
      description,
      url: alternates.canonical,
      locale: ogLocaleFor(locale),
      alternateLocale: ogAlternateLocalesFor(locale),
      images: await ogImagesFor(locale),
    },
    twitter: {
      card: seo.twitter.card,
      site: seo.twitter.site,
      title,
      description,
    },
    other: {
      robots: robotsMetaContent,
      "format-detection": "telephone=no,date=no,address=no,email=no,url=no",
      ...aiMetaTags,
    },
  };
}
