/**
 * Font Utilities
 *
 * Utility functions for extracting and managing fonts from canvas elements.
 */

import type { CanvasElement, PriceElement, VariablePriceRangeElement } from "@/types";
import { DEFAULT_FONT_VALUE } from "@/types";

/**
 * Generic font families that should not be treated as Google fonts.
 */
const GENERIC_FONTS = new Set([
  "serif",
  "sans-serif",
  "monospace",
  "cursive",
  "fantasy",
  "system-ui",
  "ui-serif",
  "ui-sans-serif",
  "ui-monospace",
  "ui-rounded",
  "emoji",
  "math",
  "fangsong",
  "inherit",
  "initial",
  "unset",
]);

/**
 * List of Google fonts we support (must match what's in AdminAssets.php).
 */
const GOOGLE_FONTS = new Set([
  "Roboto",
  "Open Sans",
  "Lato",
  "Montserrat",
  "Poppins",
  "Inter",
  "Oswald",
  "Raleway",
  "Nunito",
  "Playfair Display",
  "Merriweather",
  "Source Sans Pro",
  "Ubuntu",
  "Rubik",
  "Work Sans",
  "Fira Sans",
  "Barlow",
  "DM Sans",
  "Quicksand",
  "Libre Baskerville",
  "Crimson Text",
  "Josefin Sans",
  "Archivo",
  "Archivo Black",
  "JetBrains Mono",
  "Caveat",
  "Bebas Neue",
  "Anton",
  "Fjalla One",
  "Space Grotesk",
  "Outfit",
  "Bitter",
  "Cabin",
  "Catamaran",
  "Dosis",
  "Exo 2",
  "Heebo",
  "IBM Plex Sans",
  "Inconsolata",
  "Karla",
  "Lora",
  "Manrope",
  "Mukta",
  "Noto Sans",
  "Overpass",
  "PT Sans",
  "Questrial",
  "Righteous",
  "Signika",
  "Titillium Web",
  "Varela Round",
  "Yanone Kaffeesatz",
  "Zilla Slab",
]);

/**
 * Font variant info extracted from elements
 */
export interface FontVariant {
  weight: number;
  style: "normal" | "italic";
}

/**
 * Detailed font info with weights and styles
 */
export interface FontInfo {
  weights: number[];
  styles: ("normal" | "italic")[];
}

/**
 * Map of font names to their variants
 */
export type FontsMap = Record<string, FontInfo>;

/**
 * Responsive value structure (desktop, tablet, mobile)
 */
interface ResponsiveValue<T> {
  desktop?: T;
  tablet?: T;
  mobile?: T;
}

/**
 * Typography settings that may contain font info (with responsive values)
 */
interface TypographySettings {
  fontFamily?: string;
  fontWeight?: number | string | ResponsiveValue<number | string>;
  fontStyle?: string;
}

/**
 * Extract the primary font name from a font-family CSS value.
 * Handles values like "Inter, system-ui, sans-serif" -> "Inter"
 */
function extractPrimaryFontName(fontFamily: string): string | null {
  if (!fontFamily || fontFamily === DEFAULT_FONT_VALUE) return null;

  // Split by comma and get the first font
  const parts = fontFamily.split(",");
  const primaryFont = parts[0].trim().replace(/["']/g, "");

  // Skip if it's a generic font
  if (GENERIC_FONTS.has(primaryFont.toLowerCase())) return null;

  return primaryFont;
}

/**
 * Check if a font name is a Google font we support.
 */
function isGoogleFontName(fontName: string): boolean {
  if (GOOGLE_FONTS.has(fontName)) {
    return true;
  }

  const googleFonts = window.oppbAdmin?.fonts?.google;
  return googleFonts?.some((font) => font.value === fontName) ?? false;
}

/**
 * Check if a value is a responsive object
 */
function isResponsiveValue<T>(value: unknown): value is ResponsiveValue<T> {
  return typeof value === "object" && value !== null && ("desktop" in value || "tablet" in value || "mobile" in value);
}

/**
 * Normalize a single font weight value to a valid CSS font-weight number.
 * Handles string values like "bold", "normal", etc.
 */
function normalizeSingleWeight(weight: string | number | undefined | null): number {
  if (weight === undefined || weight === null) return 400;

  if (typeof weight === "number") {
    // Clamp to valid range and round to nearest 100
    return Math.round(Math.max(100, Math.min(900, weight)) / 100) * 100;
  }

  // Handle string values
  const weightStr = String(weight).toLowerCase().trim();

  // Map named weights to numeric values
  const namedWeights: Record<string, number> = {
    thin: 100,
    hairline: 100,
    extralight: 200,
    "extra-light": 200,
    ultralight: 200,
    "ultra-light": 200,
    light: 300,
    normal: 400,
    regular: 400,
    medium: 500,
    semibold: 600,
    "semi-bold": 600,
    demibold: 600,
    "demi-bold": 600,
    bold: 700,
    extrabold: 800,
    "extra-bold": 800,
    ultrabold: 800,
    "ultra-bold": 800,
    black: 900,
    heavy: 900,
  };

  if (namedWeights[weightStr]) {
    return namedWeights[weightStr];
  }

  // Try to parse as number
  const parsed = parseInt(weightStr, 10);
  if (!isNaN(parsed)) {
    return Math.round(Math.max(100, Math.min(900, parsed)) / 100) * 100;
  }

  return 400; // Default
}

/**
 * Extract all unique font weights from a potentially responsive fontWeight value.
 * Handles both simple values (400) and responsive objects ({desktop: 700, tablet: 600, mobile: 600}).
 */
function extractAllWeights(fontWeight: unknown): number[] {
  const weights: number[] = [];

  if (fontWeight === undefined || fontWeight === null) {
    return [400]; // Default weight
  }

  // Check if it's a responsive object
  if (isResponsiveValue<number | string>(fontWeight)) {
    const responsiveWeight = fontWeight as ResponsiveValue<number | string>;
    if (responsiveWeight.desktop !== undefined) {
      weights.push(normalizeSingleWeight(responsiveWeight.desktop));
    }
    if (responsiveWeight.tablet !== undefined) {
      weights.push(normalizeSingleWeight(responsiveWeight.tablet));
    }
    if (responsiveWeight.mobile !== undefined) {
      weights.push(normalizeSingleWeight(responsiveWeight.mobile));
    }
  } else if (typeof fontWeight === "number" || typeof fontWeight === "string") {
    // Simple value
    weights.push(normalizeSingleWeight(fontWeight));
  }

  // Return unique weights, or default if empty
  const uniqueWeights = [...new Set(weights)];
  return uniqueWeights.length > 0 ? uniqueWeights : [400];
}

/**
 * Normalize font style to valid CSS value.
 */
function normalizeFontStyle(style: string | undefined): "normal" | "italic" {
  if (!style) return "normal";
  const styleStr = String(style).toLowerCase().trim();
  return styleStr === "italic" || styleStr === "oblique" ? "italic" : "normal";
}

/**
 * Resolve typography with inheritance from a parent typography source.
 * Handles "__default__" and missing fontFamily (e.g. decimal typography that only sets fontSize).
 */
function resolveEffectiveTypography(
  typography: TypographySettings | undefined,
  inherited?: TypographySettings
): TypographySettings | undefined {
  const resolvedFamily =
    typography?.fontFamily && typography.fontFamily !== DEFAULT_FONT_VALUE
      ? typography.fontFamily
      : inherited?.fontFamily && inherited.fontFamily !== DEFAULT_FONT_VALUE
        ? inherited.fontFamily
        : undefined;

  if (!resolvedFamily) {
    return undefined;
  }

  return {
    fontFamily: resolvedFamily,
    fontWeight: typography?.fontWeight ?? inherited?.fontWeight,
    fontStyle: typography?.fontStyle ?? inherited?.fontStyle,
  };
}

/**
 * Process typography settings and add font info to the map.
 */
function processTypography(typography: TypographySettings | undefined, fontsMap: FontsMap): void {
  if (!typography?.fontFamily) return;

  const primaryFont = extractPrimaryFontName(typography.fontFamily);
  if (!primaryFont || !isGoogleFontName(primaryFont)) return;

  // Extract all weights (handles responsive values)
  const weights = extractAllWeights(typography.fontWeight);
  const style = normalizeFontStyle(typography.fontStyle);

  addFontVariants(fontsMap, primaryFont, weights, style);
}

/**
 * Add font weights and styles to the fonts map.
 */
function addFontVariants(fontsMap: FontsMap, fontName: string, weights: number[], style: "normal" | "italic"): void {
  if (!fontsMap[fontName]) {
    fontsMap[fontName] = { weights: [], styles: [] };
  }

  for (const weight of weights) {
    const weightToAdd = fontName === "Archivo Black" ? 400 : weight;
    if (!fontsMap[fontName].weights.includes(weightToAdd)) {
      fontsMap[fontName].weights.push(weightToAdd);
    }
  }

  if (!fontsMap[fontName].styles.includes(style)) {
    fontsMap[fontName].styles.push(style);
  }
}

/**
 * Extract all Google fonts with their weights and styles from canvas elements.
 * Returns a map like: { "Inter": { weights: [400, 700], styles: ["normal", "italic"] } }
 */
export function extractGoogleFontsWithVariants(elements: CanvasElement[]): FontsMap {
  const fontsMap: FontsMap = {};

  for (const element of elements) {
    const mainTypography = element.typography;

    // Main typography
    processTypography(mainTypography, fontsMap);

    // Price element decimal typography (inherits font from main when not explicitly set)
    if (element.type === "regularPrice" || element.type === "salePrice") {
      const priceEl = element as PriceElement;
      const decimalTypography = resolveEffectiveTypography(priceEl.decimal?.typography, mainTypography);
      processTypography(decimalTypography, fontsMap);
    }

    // Variable price range element
    if (element.type === "variablePriceRange") {
      const varEl = element as VariablePriceRangeElement;

      const minPriceTypography = resolveEffectiveTypography(varEl.minPriceTypography, mainTypography);
      const maxPriceTypography = resolveEffectiveTypography(varEl.maxPriceTypography, mainTypography);

      processTypography(resolveEffectiveTypography(varEl.separatorTypography, mainTypography), fontsMap);
      processTypography(minPriceTypography, fontsMap);
      processTypography(maxPriceTypography, fontsMap);
      processTypography(resolveEffectiveTypography(varEl.minDecimal?.typography, minPriceTypography ?? mainTypography), fontsMap);
      processTypography(resolveEffectiveTypography(varEl.maxDecimal?.typography, maxPriceTypography ?? mainTypography), fontsMap);
    }
  }

  // Sort weights for each font
  for (const fontName of Object.keys(fontsMap)) {
    fontsMap[fontName].weights.sort((a, b) => a - b);
  }

  return fontsMap;
}

/**
 * Extract all Google fonts used in canvas elements (simple version - just font names).
 * This includes fonts from typography settings and decimal typography.
 */
export function extractGoogleFontsFromElements(elements: CanvasElement[]): string[] {
  return Object.keys(extractGoogleFontsWithVariants(elements));
}
