/**
 * Font configuration.
 *
 * Declare fonts in `wrnexus.config.ts` under `fonts` and the framework emits
 * optimized `<head>` markup for you:
 *   - Google Fonts: `preconnect` hints + a single subsetted stylesheet request
 *     (only the weights you list) with `font-display`. The CSP is auto-extended
 *     so the fonts load under the default security policy (see loadAppConfig).
 *   - Self-hosted fonts: generated `@font-face` rules + optional `<link rel=preload>`
 *     for above-the-fold text (the fastest, no-third-party option).
 *   - Family stacks: `sans`/`mono`/`serif` become `--wrn-font-*` CSS variables,
 *     and `sans` is applied to `body`.
 */

export type FontDisplay = "auto" | "block" | "swap" | "fallback" | "optional";

export interface GoogleFont {
  /** Family name as it appears on fonts.google.com, e.g. "Inter". */
  family: string;
  /** Weights to load — ONLY these are fetched. Default: [400]. */
  weights?: (number | string)[];
  /** Also load italic styles for each weight. */
  italic?: boolean;
  /** Per-font `font-display` override (else the config default). */
  display?: FontDisplay;
}

export interface LocalFontFace {
  /** `font-family` name this face defines. */
  family: string;
  /** URL to the font file, typically served from `public/` (e.g. "/fonts/inter.woff2"). */
  src: string;
  /** e.g. 400, "700", or "100 900" for a variable font. Default: 400. */
  weight?: number | string;
  style?: "normal" | "italic";
  /** CSS `src` format; inferred from the file extension when omitted. */
  format?: string;
  display?: FontDisplay;
  /** Emit `<link rel="preload" as="font">` — use for the primary above-the-fold face. */
  preload?: boolean;
  /** Optional `unicode-range` subset. */
  unicodeRange?: string;
}

export interface FontConfig {
  /** Google Fonts, loaded with preconnect + weight subsetting + `font-display`. */
  google?: GoogleFont[];
  /** Self-hosted `@font-face` definitions (files served from `public/`). */
  local?: LocalFontFace[];
  /** Default `font-display` for faces that don't set their own. Default: "swap". */
  display?: FontDisplay;
  /** Body / default family stack → `--wrn-font-sans` + `body { font-family }`. */
  sans?: string;
  /** Monospace family stack → `--wrn-font-mono`. */
  mono?: string;
  /** Serif family stack → `--wrn-font-serif`. */
  serif?: string;
}

const GOOGLE_CSS = "https://fonts.googleapis.com";
const GOOGLE_STATIC = "https://fonts.gstatic.com";

function escAttr(s: string): string {
  return s
    .replace(/&/g, "&amp;")
    .replace(/"/g, "&quot;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;");
}

function cssString(s: string): string {
  return s
    .replace(/\\/g, "\\\\")
    .replace(/"/g, '\\"')
    .replace(/[\r\n\f]/g, " ");
}

function safeStyle(css: string): string {
  return css.replace(/<\/style/gi, "<\\/style");
}

function extOf(src: string): string {
  return (src.split(/[?#]/)[0].split(".").pop() ?? "").toLowerCase();
}

function cssFormat(src: string, override?: string): string {
  if (override) return override;
  const e = extOf(src);
  return e === "woff2"
    ? "woff2"
    : e === "woff"
      ? "woff"
      : e === "ttf"
        ? "truetype"
        : e === "otf"
          ? "opentype"
          : "woff2";
}

function preloadType(src: string): string {
  const e = extOf(src);
  return e === "woff"
    ? "font/woff"
    : e === "ttf"
      ? "font/ttf"
      : e === "otf"
        ? "font/otf"
        : "font/woff2";
}

/** Build the Google Fonts `css2` URL for the given families (weights subsetted). */
function googleFontsUrl(fonts: GoogleFont[], defDisplay: FontDisplay): string {
  const families = fonts.map((f) => {
    const name = encodeURIComponent(f.family).replace(/%20/g, "+");
    const weights = (f.weights?.length ? f.weights : [400]).map(String);
    if (f.italic) {
      const pairs = weights.flatMap((w) => [`0,${w}`, `1,${w}`]).sort((a, b) => a.localeCompare(b));
      return `family=${name}:ital,wght@${pairs.join(";")}`;
    }
    const sorted = [...weights].sort((a, b) => Number(a) - Number(b));
    return `family=${name}:wght@${sorted.join(";")}`;
  });
  return `${GOOGLE_CSS}/css2?${families.join("&")}&display=${defDisplay}`;
}

/**
 * Render all `<head>` markup for a font config. Returns "" when nothing is
 * configured. The output is trusted, framework-controlled HTML.
 */
export function renderFontHead(fonts?: FontConfig): string {
  if (!fonts) return "";
  const display = fonts.display ?? "swap";
  const out: string[] = [];

  // Google Fonts — preconnect (perf) then one subsetted stylesheet.
  if (fonts.google?.length) {
    out.push(`<link rel="preconnect" href="${GOOGLE_CSS}">`);
    out.push(`<link rel="preconnect" href="${GOOGLE_STATIC}" crossorigin>`);
    out.push(`<link rel="stylesheet" href="${escAttr(googleFontsUrl(fonts.google, display))}">`);
  }

  // Self-hosted @font-face + optional preload.
  if (fonts.local?.length) {
    const faces = fonts.local.map((f) => {
      const lines = [
        `  font-family: "${cssString(f.family)}";`,
        `  src: url("${cssString(f.src)}") format("${cssString(cssFormat(f.src, f.format))}");`,
        `  font-weight: ${f.weight ?? 400};`,
        `  font-style: ${f.style ?? "normal"};`,
        `  font-display: ${f.display ?? display};`,
      ];
      if (f.unicodeRange) lines.push(`  unicode-range: ${f.unicodeRange};`);
      return `@font-face {\n${lines.join("\n")}\n}`;
    });
    out.push(`<style>\n${safeStyle(faces.join("\n"))}\n</style>`);
    for (const f of fonts.local) {
      if (f.preload) {
        out.push(
          `<link rel="preload" href="${escAttr(f.src)}" as="font" type="${preloadType(f.src)}" crossorigin>`,
        );
      }
    }
  }

  // Family stacks → CSS variables + body default.
  const vars: string[] = [];
  if (fonts.sans) vars.push(`  --wrn-font-sans: ${fonts.sans};`);
  if (fonts.mono) vars.push(`  --wrn-font-mono: ${fonts.mono};`);
  if (fonts.serif) vars.push(`  --wrn-font-serif: ${fonts.serif};`);
  if (vars.length) {
    const body = fonts.sans ? `\nbody { font-family: var(--wrn-font-sans); }` : "";
    out.push(`<style>\n${safeStyle(`:root {\n${vars.join("\n")}\n}${body}`)}\n</style>`);
  }

  return out.join("\n    ");
}

/**
 * Production variant that inlines the small Google Fonts stylesheet at build
 * time. This removes a render-blocking CSS round trip while retaining the same
 * font files, `font-display`, CSP sources, and offline-safe fallback markup.
 */
export async function renderProductionFontHead(
  fonts?: FontConfig,
  fetcher: (input: string, init?: RequestInit) => Promise<Response> = fetch,
): Promise<string> {
  const fallback = renderFontHead(fonts);
  if (!fonts?.google?.length) return fallback;
  const url = googleFontsUrl(fonts.google, fonts.display ?? "swap");
  try {
    const response = await fetcher(url, {
      headers: {
        // Google returns compact WOFF2 rules to modern browser user agents.
        "user-agent": "Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120 Safari/537.36",
      },
    });
    if (!response.ok) return fallback;
    const css = (await response.text()).replace(/<\/style/gi, "<\\/style");
    const stylesheet = `<link rel="stylesheet" href="${escAttr(url)}">`;
    return fallback
      .replace(`<link rel="preconnect" href="${GOOGLE_CSS}">\n    `, "")
      .replace(stylesheet, `<style data-wrnexus-google-fonts>\n${css}\n</style>`);
  } catch {
    return fallback;
  }
}

/**
 * CSP source hosts required by the configured fonts, so the policy can be
 * auto-extended (Google Fonts need their CSS + static hosts allow-listed).
 */
export function fontCspSources(fonts?: FontConfig): { style: string[]; font: string[] } {
  if (fonts?.google?.length) return { style: [GOOGLE_CSS], font: [GOOGLE_STATIC] };
  return { style: [], font: [] };
}
