/**
 * @wrnexus/ssr — server-side rendering.
 *
 * Pages return an HTML string for the body; this module wraps that body in a
 * full document with a `<head>` built from page metadata. It is intentionally
 * isolated from any client runtime: nothing here touches the DOM or ships to
 * the browser, which keeps "server-only code" genuinely server-only.
 */

import { escapeHtml, type PageMeta, type SeoConfig } from "@wrnexus/core";

export interface RenderOptions {
  /** Page metadata for the document head. */
  meta: PageMeta;
  /** Global SEO defaults from `wrnexus.config.ts`. */
  seo?: SeoConfig;
  /** Current request URL, used to resolve canonical/Open Graph URLs. */
  url?: URL;
  /** Rendered HTML for the body (placed inside `#app`). */
  body: string;
  /**
   * URLs of `<script type="module">` tags to load (e.g. per-island chunks or
   * the reactive runtime). Only the scripts a page actually needs are passed.
   */
  scripts?: string[];
  /** Optional default document title used when meta.title is absent. */
  defaultTitle?: string;
  /** Raw HTML injected at the end of `<head>` (trusted, framework-controlled). */
  extraHead?: string;
  /** Raw HTML injected at the end of `<body>` (trusted, framework-controlled). */
  extraBody?: string;
  /** Attributes for the `<html>` element, e.g. ` data-theme="dark"` (trusted). */
  htmlAttrs?: string;
}

/**
 * Render a complete HTML document.
 *
 * Metadata is HTML-escaped so a malicious title/description can never break
 * out of its element or attribute.
 */
export function renderDocument(opts: RenderOptions): string {
  const seo = mergeSeo(opts.seo, opts.meta, opts.defaultTitle, opts.url);
  const title = escapeHtml(seo.title);
  const scripts = [...new Set(opts.scripts ?? [])];

  const seoTags = renderSeoTags(seo);
  const preloadTags = scripts
    .map((src) => `\n    <link rel="modulepreload" href="${escapeHtml(src)}" />`)
    .join("");
  const scriptTags = scripts
    .map((src) => `\n    <script type="module" src="${escapeHtml(src)}"></script>`)
    .join("");

  const extraHead = opts.extraHead ? `\n    ${opts.extraHead}` : "";
  const extraBody = opts.extraBody ? `\n    ${opts.extraBody}` : "";

  const htmlAttrs = /(?:^|\s)lang\s*=/.test(opts.htmlAttrs ?? "")
    ? (opts.htmlAttrs ?? "")
    : `${opts.htmlAttrs ?? ""} lang="en"`;

  return `<!doctype html>
<html${htmlAttrs}>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" href="/favicon.ico" />
    <title>${title}</title>${seoTags}${preloadTags}${extraHead}
  </head>
  <body>
    <div id="app">${opts.body}</div>${scriptTags}${extraBody}
  </body>
</html>
`;
}

interface ResolvedSeo {
  title: string;
  description?: string;
  canonical?: string;
  robots?: string;
  keywords?: string;
  image?: string;
  siteName?: string;
  type: string;
  locale?: string;
  twitterCard: string;
  twitterSite?: string;
  themeColor?: string;
}

function mergeSeo(
  globalSeo: SeoConfig | undefined,
  pageSeo: PageMeta,
  defaultTitle: string | undefined,
  url: URL | undefined,
): ResolvedSeo {
  const pageTitle = pageSeo.title;
  const baseTitle = pageTitle ?? globalSeo?.title ?? defaultTitle ?? "WrNexus";
  const titleTemplate = pageTitle ? globalSeo?.titleTemplate : undefined;
  const title = titleTemplate?.includes("%s") ? titleTemplate.replace("%s", baseTitle) : baseTitle;

  const canonical = resolveSeoUrl(
    pageSeo.canonical ??
      globalSeo?.canonical ??
      (globalSeo?.canonicalBase && url ? url.pathname : undefined),
    pageSeo.canonicalBase ?? globalSeo?.canonicalBase,
    url,
  );
  const image = resolveSeoUrl(
    pageSeo.image ?? globalSeo?.image,
    pageSeo.canonicalBase ?? globalSeo?.canonicalBase,
    url,
  );
  const keywords = pageSeo.keywords ?? globalSeo?.keywords;

  return {
    title,
    description: pageSeo.description ?? globalSeo?.description,
    canonical,
    robots: pageSeo.robots ?? globalSeo?.robots,
    keywords: Array.isArray(keywords) ? keywords.join(", ") : keywords,
    image,
    siteName: pageSeo.siteName ?? globalSeo?.siteName,
    type: pageSeo.type ?? globalSeo?.type ?? "website",
    locale: pageSeo.locale ?? globalSeo?.locale,
    twitterCard: pageSeo.twitterCard ?? globalSeo?.twitterCard ?? "summary",
    twitterSite: pageSeo.twitterSite ?? globalSeo?.twitterSite,
    themeColor: pageSeo.themeColor ?? globalSeo?.themeColor,
  };
}

function renderSeoTags(seo: ResolvedSeo): string {
  const tags: string[] = [];

  if (seo.description) tags.push(meta("description", seo.description));
  if (seo.robots) tags.push(meta("robots", seo.robots));
  if (seo.keywords) tags.push(meta("keywords", seo.keywords));
  if (seo.themeColor) tags.push(meta("theme-color", seo.themeColor));
  if (seo.canonical) tags.push(`<link rel="canonical" href="${escapeHtml(seo.canonical)}" />`);

  tags.push(property("og:title", seo.title));
  if (seo.description) tags.push(property("og:description", seo.description));
  tags.push(property("og:type", seo.type));
  if (seo.canonical) tags.push(property("og:url", seo.canonical));
  if (seo.siteName) tags.push(property("og:site_name", seo.siteName));
  if (seo.locale) tags.push(property("og:locale", seo.locale));
  if (seo.image) tags.push(property("og:image", seo.image));

  tags.push(meta("twitter:card", seo.twitterCard));
  tags.push(meta("twitter:title", seo.title));
  if (seo.description) tags.push(meta("twitter:description", seo.description));
  if (seo.image) tags.push(meta("twitter:image", seo.image));
  if (seo.twitterSite) tags.push(meta("twitter:site", seo.twitterSite));

  return tags.length ? `\n    ${tags.join("\n    ")}` : "";
}

function meta(name: string, content: string): string {
  return `<meta name="${escapeHtml(name)}" content="${escapeHtml(content)}" />`;
}

function property(name: string, content: string): string {
  return `<meta property="${escapeHtml(name)}" content="${escapeHtml(content)}" />`;
}

function resolveSeoUrl(
  value: string | undefined,
  base: string | undefined,
  currentUrl: URL | undefined,
): string | undefined {
  if (!value) return undefined;
  try {
    const origin = base ?? (currentUrl ? currentUrl.origin : undefined);
    return origin ? new URL(value, origin).toString() : value;
  } catch {
    return value;
  }
}
