import { readFileSync } from 'node:fs';
import { defineConfig, definePreset } from 'unocss';
import presetWind from '@unocss/preset-wind4';
import presetAnimations from 'unocss-preset-animations';
import presetShadcn from 'unocss-preset-shadcn';
import { tokenPreset as sparkTokenPreset } from '@aleph-alpha/config-css';

let tokensCache;

let saansFontFaceCssCache;
const getSaansFontFaceCss = () => {
  if (saansFontFaceCssCache) return saansFontFaceCssCache;

  const fontsDir = new URL('./src/assets/fonts/Saans/', import.meta.url);
  const fonts = [
    { file: 'Saans-Regular.woff2', weight: 400 },
    { file: 'Saans-Medium.woff2', weight: 500 },
    { file: 'Saans-SemiBold.woff2', weight: 600 },
    { file: 'Saans-Bold.woff2', weight: 700 },
  ];

  saansFontFaceCssCache = fonts
    .map(({ file, weight }) => {
      const buffer = readFileSync(new URL(file, fontsDir));
      const base64 = buffer.toString('base64');
      return `@font-face {
  font-display: swap;
  font-family: 'Saans';
  font-style: normal;
  font-weight: ${weight};
  src: url('data:font/woff2;base64,${base64}') format('woff2');
}`;
    })
    .join('\n');

  return saansFontFaceCssCache;
};

const getTokens = () => {
  if (tokensCache) return tokensCache;
  const raw = readFileSync(new URL('./tokens.json', import.meta.url), 'utf8');
  tokensCache = JSON.parse(raw);
  return tokensCache;
};

// Color conversion utilities
const clamp01 = (n) => (Number.isFinite(n) ? Math.min(1, Math.max(0, n)) : 0);
const roundTo = (n, decimals) =>
  Math.round(n * 10 ** decimals) / 10 ** decimals;
const srgbToLinear = (c) =>
  c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;

const parseHexColorToRgba = (hex) => {
  const s = hex.trim();
  if (!s.startsWith('#')) return null;
  const h = s.slice(1);
  if (![3, 6, 8].includes(h.length) || !/^[0-9a-fA-F]+$/.test(h)) return null;
  if (h.length === 3) {
    return {
      r: parseInt(h[0] + h[0], 16),
      g: parseInt(h[1] + h[1], 16),
      b: parseInt(h[2] + h[2], 16),
      a: 1,
    };
  }
  const result = {
    r: parseInt(h.slice(0, 2), 16),
    g: parseInt(h.slice(2, 4), 16),
    b: parseInt(h.slice(4, 6), 16),
    a: 1,
  };
  // 8-character hex includes alpha as last 2 characters
  if (h.length === 8) {
    result.a = parseInt(h.slice(6, 8), 16) / 255;
  }
  return result;
};

const rgbToOklchComponents = (r, g, b, alpha = 1) => {
  const lr = srgbToLinear(clamp01(r / 255));
  const lg = srgbToLinear(clamp01(g / 255));
  const lb = srgbToLinear(clamp01(b / 255));

  const l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
  const m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
  const s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;

  const l_ = Math.cbrt(l);
  const m_ = Math.cbrt(m);
  const s_ = Math.cbrt(s);

  const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
  const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
  const b2 = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;

  const C = Math.sqrt(a * a + b2 * b2);
  let h = Math.atan2(b2, a) * (180 / Math.PI);
  if (h < 0) h += 360;

  const base = `${roundTo(L * 100, 2)}% ${roundTo(C, 4)} ${roundTo(h, 2)}`;
  // Include alpha if not fully opaque
  if (alpha < 1) {
    return `${base} / ${roundTo(alpha * 100, 0)}%`;
  }
  return base;
};

const hslToRgb = (h, s, l) => {
  const hh = ((h % 360) + 360) % 360;
  const c = (1 - Math.abs(2 * l - 1)) * s;
  const x = c * (1 - Math.abs(((hh / 60) % 2) - 1));
  const m = l - c / 2;
  let r1 = 0,
    g1 = 0,
    b1 = 0;

  if (hh < 60) [r1, g1, b1] = [c, x, 0];
  else if (hh < 120) [r1, g1, b1] = [x, c, 0];
  else if (hh < 180) [r1, g1, b1] = [0, c, x];
  else if (hh < 240) [r1, g1, b1] = [0, x, c];
  else if (hh < 300) [r1, g1, b1] = [x, 0, c];
  else [r1, g1, b1] = [c, 0, x];

  return {
    r: Math.round((r1 + m) * 255),
    g: Math.round((g1 + m) * 255),
    b: Math.round((b1 + m) * 255),
  };
};

const hslComponentsToRgb = (raw) => {
  const m = raw.match(
    /^(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)%\s+(\d+(?:\.\d+)?)%\s*$/,
  );
  if (!m) return null;
  const h = Number(m[1]);
  const s = Number(m[2]) / 100;
  const l = Number(m[3]) / 100;
  if (!Number.isFinite(h) || !Number.isFinite(s) || !Number.isFinite(l))
    return null;
  return hslToRgb(h, s, l);
};

const normalizeShadcnVarValue = (input) => {
  if (input == null) return '';
  const v = String(input).trim();

  const oklchMatch = v.match(/^oklch\(\s*([^)]+?)\s*\)$/i);
  if (oklchMatch?.[1]) return oklchMatch[1].trim();

  const hslMatch = v.match(/^hsl\(\s*([^)]+?)\s*\)$/i);
  if (hslMatch?.[1]) {
    const rgb = hslComponentsToRgb(hslMatch[1].trim());
    if (rgb) return rgbToOklchComponents(rgb.r, rgb.g, rgb.b);
    return hslMatch[1].trim();
  }

  const rawHslRgb = hslComponentsToRgb(v);
  if (rawHslRgb)
    return rgbToOklchComponents(rawHslRgb.r, rawHslRgb.g, rawHslRgb.b);

  const rgbMatch = v.match(
    /^rgba?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)(?:\s*,\s*(\d+(?:\.\d+)?))?\s*\)$/i,
  );
  if (rgbMatch) {
    const alpha = rgbMatch[4] !== undefined ? Number(rgbMatch[4]) : 1;
    return rgbToOklchComponents(
      Number(rgbMatch[1]),
      Number(rgbMatch[2]),
      Number(rgbMatch[3]),
      alpha,
    );
  }

  const hexRgba = parseHexColorToRgba(v);
  if (hexRgba)
    return rgbToOklchComponents(hexRgba.r, hexRgba.g, hexRgba.b, hexRgba.a);

  return v;
};

// Token resolution — one selected `2-theme/*` set participates in `{...}` lookups;
// other theme sets are alternate brands/modes and must not override the selected
// library theme.
const UI_LIBRARY_REFERENCE_THEME_SETS = Object.freeze({
  default: '2-theme/AA Base',
  accessible: '2-theme/WCAG 2.2',
});

const getUiLibraryThemeMode = (options = {}) => {
  if (options.referenceThemeSet) return options.referenceThemeSet;
  if (options.accessible || options.themeMode === 'accessible') {
    return UI_LIBRARY_REFERENCE_THEME_SETS.accessible;
  }
  return UI_LIBRARY_REFERENCE_THEME_SETS.default;
};

const getTokenSetSearchOrder = (options = {}) => {
  const tokens = getTokens();
  const order = tokens?.$metadata?.tokenSetOrder;
  const raw = Array.isArray(order)
    ? order
    : Object.keys(tokens || {}).filter((k) => !k.startsWith('$'));
  const referenceThemeSet = getUiLibraryThemeMode(options);
  const fallbackThemeSet = UI_LIBRARY_REFERENCE_THEME_SETS.default;
  return raw.filter(
    (name) =>
      !name.startsWith('2-theme/') ||
      name === referenceThemeSet ||
      (referenceThemeSet === UI_LIBRARY_REFERENCE_THEME_SETS.accessible &&
        name === fallbackThemeSet),
  );
};

const getTokenLeaf = (tokenSetName, dotPath) => {
  const tokens = getTokens();
  const set = tokens?.[tokenSetName];
  if (!set || typeof set !== 'object') return null;

  let cur = set;
  for (const p of dotPath.split('.')) {
    if (!cur || typeof cur !== 'object') return null;
    cur = cur[p];
  }
  return cur && typeof cur === 'object' && 'value' in cur ? cur : null;
};

const resolveReferences = (value, seen, options = {}) => {
  if (typeof value !== 'string') return value;

  const refMatch = value.match(/^\{(.+)\}$/);
  if (!refMatch) return value;

  const tokens = getTokens();
  const refPath = refMatch[1].trim();
  const marker = `ref:${refPath}`;

  if (seen.has(marker)) {
    throw new Error(
      `Circular token reference: ${[...seen].join(' -> ')} -> ${refPath}`,
    );
  }
  seen.add(marker);

  const parts = refPath.split('.');
  const sets = getTokenSetSearchOrder(options).slice().reverse();

  for (const setName of sets) {
    const set = tokens?.[setName];
    if (!set || typeof set !== 'object') continue;

    let cur = set;
    for (const p of parts) {
      if (!cur || typeof cur !== 'object') {
        cur = null;
        break;
      }
      cur = cur[p];
    }

    if (cur && typeof cur === 'object' && 'value' in cur) {
      return resolveReferences(cur.value, seen, options);
    }
  }
  return value;
};

const resolveTokenValue = (tokenSetName, path, options = {}) => {
  const leaf = getTokenLeaf(tokenSetName, path);
  if (!leaf) return null;
  const resolved = resolveReferences(
    leaf.value,
    new Set([`${tokenSetName}:${path}`]),
    options,
  );
  return typeof resolved === 'string' ? resolved : null;
};

const resolveAnyTokenValue = (path, options = {}) => {
  const resolved = resolveReferences(
    `{${path}}`,
    new Set([`any:${path}`]),
    options,
  );
  return typeof resolved === 'string' ? resolved : null;
};

// Collect all color token paths from a mode set
const collectColorTokenPaths = (obj, prefix = '') => {
  const paths = [];
  for (const [key, value] of Object.entries(obj)) {
    const path = prefix ? `${prefix}.${key}` : key;
    if (
      value &&
      typeof value === 'object' &&
      'value' in value &&
      value.type === 'color'
    ) {
      paths.push(path);
    } else if (value && typeof value === 'object' && !('value' in value)) {
      paths.push(...collectColorTokenPaths(value, path));
    }
  }
  return paths;
};

// Build all token-based CSS variables (e.g., --background-surface-secondary)
const buildAllTokenVarsFromModeSet = (modeSetName, options = {}) => {
  const tokens = getTokens();
  const modeSet = tokens?.[modeSetName];
  if (!modeSet) return {};

  const paths = collectColorTokenPaths(modeSet);
  const vars = {};

  for (const path of paths) {
    const value = resolveTokenValue(modeSetName, path, options);
    if (value) {
      // Convert dot path to kebab-case CSS variable name
      // e.g., "background.surface.secondary" -> "background-surface-secondary"
      const varName = path.replace(/\./g, '-');
      vars[varName] = value;
    }
  }

  return vars;
};

// shadcn CSS variable builders
const buildShadcnVarsFromModeSet = (modeSetName, options = {}) => {
  const resolve = (path) => resolveTokenValue(modeSetName, path, options);

  // Background/Surface tokens
  const background = resolve('background.surface.primary');
  const foreground = resolve('content.on-surface.primary');

  // Primary - main brand/action button color
  const primary = resolve('background.button.primary-button');
  const primaryForeground =
    resolve('content.on-button.primary-button') ?? foreground;

  // Secondary - less prominent actions
  const secondary =
    resolve('background.button.secondary-button') ??
    resolve('background.surface.secondary');
  const secondaryForeground =
    resolve('content.on-button.secondary-button') ?? foreground;

  // Muted - de-emphasized surfaces
  const muted =
    resolve('background.surface.muted') ??
    resolve('background.surface.secondary');
  const mutedForeground =
    resolve('content.on-surface.muted') ??
    resolve('content.on-surface.tertiary') ??
    foreground;

  // Accent - highlight/focus elements
  const accent =
    resolve('background.accent.soft') ?? resolve('background.accent.default');
  const accentForeground =
    resolve('content.on-accent.soft') ??
    resolve('content.on-accent.default') ??
    foreground;
  const accentDefault = resolve('background.accent.default');
  const accentDefaultForeground =
    resolve('content.on-accent.default') ?? foreground;

  // Destructive - danger/error actions
  const destructive = resolve('background.button.danger-button');
  const destructiveForeground =
    resolve('content.on-button.danger-button') ?? foreground;

  // Border tokens
  const border =
    resolve('border.surface.default') ?? resolve('border.input.default');
  const input = resolve('border.input.default') ?? border;
  const ring =
    resolve('border.surface.focus-ring') ??
    resolve('background.accent.default');

  // Surface tokens
  const modal = resolve('background.surface.modal') ?? background;
  const floating =
    resolve('background.surface.floating') ??
    resolveAnyTokenValue('aa.colors.sand.0', options) ??
    resolve('background.surface.secondary') ??
    background;

  // Popover/Card surfaces
  const popover = floating;
  const card = resolve('background.surface.primary') ?? background;

  // Input background
  const inputBg = resolve('background.input.default');

  // Link color
  const link = resolve('content.on-surface.link') ?? foreground;

  // Hover overlays
  const hoverDefault = resolve('overlay.hover.default-hover');

  // Overlay hover surfaces (3-mode overlay.hover → utilities: bg-overlay-hover-*)
  const overlayHoverDefault = resolve('overlay.hover.default');
  const overlayHoverSubtle = resolve('overlay.hover.subtle');
  const overlayHoverStrong = resolve('overlay.hover.strong');

  // Modal overlay
  const modalOverlay = resolve('overlay.surface.modal-overlay');

  // Inverse surfaces (for tooltips)
  const inverse = resolve('background.surface.inverse');
  const inverseForeground = resolve('content.on-surface.inverse') ?? foreground;

  // Secondary content color (for calendar unselected dates)
  const secondaryContent =
    resolve('content.on-surface.secondary') ?? foreground;

  // Sidebar tokens (overrides unocss-preset-shadcn defaults with design-system colors)
  const sidebar = resolve('background.surface.primary') ?? background;
  const sidebarForeground = foreground;
  const sidebarPrimary = primary;
  const sidebarPrimaryForeground = primaryForeground;
  const sidebarAccent = muted;
  const sidebarAccentForeground = foreground;
  const sidebarBorder = border;
  const sidebarRing = ring;

  return {
    background,
    foreground,
    modal,
    card,
    'card-foreground': foreground,
    popover,
    'popover-foreground': foreground,
    primary,
    'primary-foreground': primaryForeground,
    secondary,
    'secondary-foreground': secondaryForeground,
    muted,
    'muted-foreground': mutedForeground,
    accent,
    'accent-foreground': accentForeground,
    'accent-default': accentDefault,
    'accent-default-foreground': accentDefaultForeground,
    destructive,
    'destructive-foreground': destructiveForeground,
    border,
    input,
    'input-background': inputBg,
    ring,
    'ring-offset': background,
    link,
    'hover-default': hoverDefault,
    'overlay-hover-default': overlayHoverDefault,
    'overlay-hover-subtle': overlayHoverSubtle,
    'overlay-hover-strong': overlayHoverStrong,
    'modal-overlay': modalOverlay,
    inverse,
    'inverse-foreground': inverseForeground,
    'secondary-content': secondaryContent,
    sidebar,
    'sidebar-background': sidebar,
    'sidebar-foreground': sidebarForeground,
    'sidebar-primary': sidebarPrimary,
    'sidebar-primary-foreground': sidebarPrimaryForeground,
    'sidebar-accent': sidebarAccent,
    'sidebar-accent-foreground': sidebarAccentForeground,
    'sidebar-border': sidebarBorder,
    'sidebar-ring': sidebarRing,
  };
};

const mergeThemes = (base, extend) => {
  if (!extend) return base;
  return {
    light: { ...base.light, ...extend.light },
    dark: { ...base.dark, ...extend.dark },
  };
};

const normalizeRootFontSize = (value) => {
  if (value == null) return undefined;
  if (typeof value === 'number' && Number.isFinite(value)) {
    return `${value}px`;
  }
  if (typeof value !== 'string') return undefined;
  const trimmed = value.trim();
  return trimmed || undefined;
};

const isAccessibleThemeMode = (options = {}) =>
  getUiLibraryThemeMode(options) === UI_LIBRARY_REFERENCE_THEME_SETS.accessible;

const getAccessibleTokenVarOverrides = (options = {}) => {
  if (!isAccessibleThemeMode(options)) {
    return { light: {}, dark: {} };
  }

  return {
    light: {
      'background-status-warning':
        resolveAnyTokenValue('aa.colors.amber.800', options) ?? '#974e00',
      'content-on-surface-tertiary':
        resolveAnyTokenValue('aa.colors.orange.700', options) ?? '#b83e16',
    },
    dark: {},
  };
};

// Exported functions
export const getUiLibraryContentConfig = () => ({
  content: {
    filesystem: ['node_modules/@aleph-alpha/ui-library/dist/**/*.js'],
    pipeline: {
      include: [
        /\.(vue|svelte|[jt]sx|mdx?|astro|elm|php|phtml|html)($|\?)/,
        'src/**/*.{js,ts,vue}',
        'node_modules/@aleph-alpha/ui-library/**/*.js',
      ],
    },
  },
});

export const getUiLibraryThemeFromTokens = (options = {}) => ({
  light: buildShadcnVarsFromModeSet('3-mode/Light', options),
  dark: buildShadcnVarsFromModeSet('3-mode/Dark', options),
});

/**
 * Get all token-based CSS variables for light and dark modes.
 * These map directly to the token structure, e.g.:
 * - background.surface.secondary -> --background-surface-secondary
 * - content.on-surface.primary -> --content-on-surface-primary
 * @param {object} [options] Token resolution options.
 * @returns {{ light: Record<string, string>, dark: Record<string, string> }} Token CSS variables by mode.
 */
export const getUiLibraryTokenVars = (options = {}) => {
  const accessibleVars = getAccessibleTokenVarOverrides(options);
  return {
    light: {
      ...buildAllTokenVarsFromModeSet('3-mode/Light', options),
      ...accessibleVars.light,
    },
    dark: {
      ...buildAllTokenVarsFromModeSet('3-mode/Dark', options),
      ...accessibleVars.dark,
    },
  };
};

/**
 * Get UnoCSS theme colors config for all token-based colors.
 * This allows using classes like bg-background-surface-secondary, text-content-on-surface-primary, etc.
 * @example
 * ```ts
 * // uno.config.ts
 * import { getUiLibraryTokenColors } from '@aleph-alpha/ui-library/config';
 *
 * export default defineConfig({
 *   theme: {
 *     colors: {
 *       ...getUiLibraryTokenColors(),
 *     },
 *   },
 * });
 * ```
 * @returns {Record<string, string>} UnoCSS color entries keyed by token path.
 */
export const getUiLibraryTokenColors = () => {
  const lightVars = buildAllTokenVarsFromModeSet('3-mode/Light');
  const colors = {};

  for (const varName of Object.keys(lightVars)) {
    // Map CSS variable name to UnoCSS color using oklch with the variable
    colors[varName] = `oklch(var(--${varName}))`;
  }

  return colors;
};

const getCornerRadiusTokenGroup = () => {
  const primitives = getTokens()?.['1-primitives/Default'];
  return (
    primitives?.aa?.['corner-radius'] ??
    primitives?.aa26?.['corner-radius'] ??
    null
  );
};

const borderRadiusKeyFromTokenName = (tokenName) => {
  const key = tokenName.replace(/^rounded-/, '');
  return key === 'md' ? ['DEFAULT', key] : [key];
};

/**
 * Get UnoCSS border-radius config from corner-radius design tokens.
 * This powers classes like rounded-xs, rounded-md, rounded-4xl, and rounded-full.
 * @returns {Record<string, string>} UnoCSS border-radius entries keyed by radius name.
 */
export const getUiLibraryTokenBorderRadius = () => {
  const cornerRadius = getCornerRadiusTokenGroup();
  if (!cornerRadius) return {};

  const borderRadius = {};
  for (const [tokenName, tokenData] of Object.entries(cornerRadius)) {
    if (
      !tokenData ||
      typeof tokenData !== 'object' ||
      !('value' in tokenData)
    ) {
      continue;
    }

    const resolved = resolveReferences(tokenData.value, new Set());
    if (typeof resolved !== 'string' || !resolved.trim()) continue;

    for (const key of borderRadiusKeyFromTokenName(tokenName)) {
      borderRadius[key] = resolved;
    }
  }

  return borderRadius;
};

const getUiLibraryTokenBorderRadiusVars = () =>
  Object.fromEntries(
    Object.entries(getUiLibraryTokenBorderRadius()).map(([key, value]) => [
      `radius-${key}`,
      value,
    ]),
  );

const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

const entriesToRules = (entries) =>
  entries.map(([className, declarations]) => [
    new RegExp(`^${escapeRegExp(className)}$`),
    () => declarations,
  ]);

const getBorderRadiusExactUtilityRules = () => {
  const entries = [];
  for (const [key, value] of Object.entries(getUiLibraryTokenBorderRadius())) {
    const className = key === 'DEFAULT' ? 'rounded' : `rounded-${key}`;
    entries.push([className, { 'border-radius': value }]);
  }
  return entriesToRules(entries);
};

export const defineThemeConfig = (config) => config;

export const presetUiLibraryTheme = (theme, options = {}) => {
  const rootSelector = options.rootSelector ?? ':root';
  const darkSelector = options.darkSelector ?? 'html.dark';
  const rootFontSize = normalizeRootFontSize(options.rootFontSize);

  const opacityDefaults = `
*,
::before,
::after {
  --un-bg-opacity: 1;
  --un-text-opacity: 1;
  --un-border-opacity: 1;
  --un-ring-opacity: 1;
  --un-shadow-opacity: 1;
}`.trim();

  const toCssBlock = (selector, vars, extraDeclarations = []) => {
    const entries = Object.entries(vars || {}).filter(
      ([k]) => k && typeof k === 'string',
    );
    const lines = [
      ...extraDeclarations,
      ...entries.map(([k, v]) => `  --${k}: ${normalizeShadcnVarValue(v)};`),
    ].join('\n');
    if (!lines) return '';
    return `${selector} {\n${lines}\n}\n`;
  };

  const css = `${opacityDefaults}\n${toCssBlock(
    rootSelector,
    theme.light,
    rootFontSize ? [`  font-size: ${rootFontSize};`] : [],
  )}${toCssBlock(darkSelector, theme.dark)}`.trim();

  return definePreset(() => ({
    name: '@aleph-alpha/ui-library-theme',
    preflights: [{ getCSS: () => css }],
  }))();
};

export const presetUiLibraryThemeFromTokens = (options = {}) => {
  const {
    theme,
    extend,
    includeAllTokens = true,
    themeMode = 'default',
    accessible = false,
    accessibleTheme = false,
    accessibleSelector = 'html.accessible-theme',
    ...presetOptions
  } = options;

  // If full theme override provided, use basic preset (no batteries)
  if (theme) {
    return presetUiLibraryTheme(theme, presetOptions);
  }

  const tokenBorderRadiusVars = getUiLibraryTokenBorderRadiusVars();
  const buildTheme = (modeOptions = {}) => {
    const baseTheme = getUiLibraryThemeFromTokens(modeOptions);
    let resolvedTheme = {
      light: { ...baseTheme.light, ...tokenBorderRadiusVars },
      dark: { ...baseTheme.dark, ...tokenBorderRadiusVars },
    };

    if (includeAllTokens) {
      const tokenVars = getUiLibraryTokenVars(modeOptions);
      resolvedTheme = {
        light: { ...tokenVars.light, ...resolvedTheme.light },
        dark: { ...tokenVars.dark, ...resolvedTheme.dark },
      };
    }

    return resolvedTheme;
  };

  // Radius utilities in Wind v4 resolve through --radius-* variables.
  const modeOptions = { themeMode, accessible };
  let finalTheme = buildTheme(modeOptions);

  // Merge with extend overrides
  finalTheme = mergeThemes(finalTheme, extend);

  // Get base preset with preflights (CSS variables)
  const basePreset = presetUiLibraryTheme(finalTheme, presetOptions);
  const accessiblePreset = accessibleTheme
    ? presetUiLibraryTheme(buildTheme({ themeMode: 'accessible' }), {
        ...presetOptions,
        rootSelector: accessibleSelector,
        darkSelector: `${accessibleSelector}.dark`,
      })
    : null;

  // Token colors for utility classes (bg-background-surface-secondary, etc.)
  const tokenColors = getUiLibraryTokenColors();

  // Semantic color overrides used by components
  const semanticColors = {
    background: 'oklch(var(--background))',
    foreground: 'oklch(var(--foreground))',
    modal: 'oklch(var(--modal))',
    link: 'oklch(var(--link))',
    'hover-default': 'oklch(var(--hover-default))',
    'overlay-hover-default': 'oklch(var(--overlay-hover-default))',
    'overlay-hover-subtle': 'oklch(var(--overlay-hover-subtle))',
    'overlay-hover-strong': 'oklch(var(--overlay-hover-strong))',
    'modal-overlay': 'oklch(var(--modal-overlay))',
    inverse: 'oklch(var(--inverse))',
    'inverse-foreground': 'oklch(var(--inverse-foreground))',
    'secondary-content': 'oklch(var(--secondary-content))',
    'input-background': 'oklch(var(--input-background))',
    'accent-default': 'oklch(var(--accent-default))',
    'accent-default-foreground': 'oklch(var(--accent-default-foreground))',
  };

  // Typography shortcuts from design tokens
  const typographyShortcuts = getUiLibraryTypographyShortcuts();

  // Resolve font families from tokens for --font-sans / --font-heading overrides
  const fontBody = resolveAnyTokenValue('font.font-family.font-body');
  const fontHeading = resolveAnyTokenValue('font.font-family.font-heading');
  const fontVariablesCss = `
:root, :host {
  --font-sans: '${fontBody}', sans-serif;
  --font-heading: '${fontHeading}', sans-serif;
}`.trim();

  // Return preset with token-related config: CSS variables, token colors, semantic overrides, typography, fonts
  return {
    ...basePreset,
    preflights: [
      ...(basePreset.preflights || []),
      ...(accessiblePreset?.preflights || []),
      { getCSS: () => getSaansFontFaceCss() },
      { getCSS: () => fontVariablesCss },
    ],
    theme: {
      font: {
        sans: `'${fontBody}', sans-serif`,
        heading: `'${fontHeading}', sans-serif`,
      },
      colors: {
        ...tokenColors,
        ...semanticColors,
      },
    },
    rules: [
      ...getBorderRadiusExactUtilityRules(),
      // Typography classes from design tokens (heading-48-bold, body-14-normal, label-12-medium, etc.)
      ...typographyShortcuts.map(([name, css]) => [
        new RegExp(`^${name}$`),
        () =>
          Object.fromEntries(
            css.split(';').map((p) => p.split(':').map((s) => s.trim())),
          ),
      ]),
    ],
  };
};

/** Overlay tier names for the `v-hover` directive (token-backed hover layer). */
export const vHoverOverlayVariants = Object.freeze({
  subtle: 'subtle',
  default: 'default',
  strong: 'strong',
});

const vHoverOverlayCSS = `
.v-hover-root {
  position: relative;
  overflow: hidden;
}
.v-hover-overlay {
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.v-hover-root > :not(.v-hover-overlay) {
  position: relative;
  z-index: 1;
}
.v-hover-overlay[data-v-hover-variant="subtle"] {
  background-color: oklch(var(--overlay-hover-subtle, 0% 0 0 / 20%));
}
.v-hover-overlay[data-v-hover-variant="default"] {
  background-color: oklch(var(--overlay-hover-default, 0% 0 0 / 50%));
}
.v-hover-overlay[data-v-hover-variant="strong"] {
  background-color: oklch(var(--overlay-hover-strong, 0% 0 0 / 70%));
}
@media (hover: hover) {
  .v-hover-root:hover .v-hover-overlay {
    opacity: 1;
  }
}
.v-hover-root[data-v-overlay-active] .v-hover-overlay {
  opacity: 1;
}
.v-hover-root:has(:focus-visible) .v-hover-overlay {
  opacity: 1;
}
.v-hover-root:active .v-hover-overlay {
  opacity: 1;
}
.hover-layer {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}
.hover-layer::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  background-color: var(--hover-layer-background, oklch(var(--overlay-hover-default)));
  transition: opacity 0.2s ease;
}
.hover-layer > :not(.v-hover-overlay) {
  position: relative;
  z-index: 1;
}
.hover-layer-subtle {
  --hover-layer-background: oklch(var(--overlay-hover-subtle));
}
.hover-layer-default {
  --hover-layer-background: oklch(var(--overlay-hover-default));
}
.hover-layer-strong {
  --hover-layer-background: oklch(var(--overlay-hover-strong));
}
@media (hover: hover) {
  .hover-layer:hover::after {
    opacity: 1;
  }
}
.hover-layer:focus::after,
.hover-layer:focus-visible::after,
.hover-layer:has(:focus-visible)::after,
.hover-layer:active::after,
.hover-layer-active::after,
.hover-layer[data-highlighted]::after,
.hover-layer[data-state="open"]::after,
.hover-layer[data-active="true"]::after {
  opacity: 1;
}
.ui-active-indicator {
  position: relative;
}
.ui-active-indicator::before {
  content: "";
  position: absolute;
  z-index: 10;
  top: 50%;
  left: 0;
  width: 0.125rem;
  height: 50%;
  pointer-events: none;
  border-radius: 9999px;
  background-color: oklch(var(--background-accent-default));
  opacity: 0;
  transform: translateY(-50%) scaleX(0);
  transform-origin: left;
  transition:
    transform 200ms linear,
    opacity 200ms linear;
}
.ui-active-indicator[data-active="true"]::before,
.ui-active-indicator-active::before {
  opacity: 1;
  transform: translateY(-50%) scaleX(1);
}
.hover-layer:disabled::after,
.hover-layer[aria-disabled="true"]::after,
.hover-layer[data-disabled]::after {
  opacity: 0;
  transition: none;
}
@media (prefers-reduced-motion: reduce) {
  .v-hover-overlay,
  .hover-layer::after,
  .ui-active-indicator::before {
    transition: none;
  }
}
/* Link variant: mix subtle overlay into text color (not background overlay). */
.overlay-link-hover-subtle {
  transition: color 0.2s ease;
}
@media (hover: hover) {
  .overlay-link-hover-subtle:hover {
    color: color-mix(
      in oklch,
      oklch(var(--content-on-surface-link, var(--link))) 88%,
      oklch(var(--overlay-hover-subtle, 0% 0 0 / 20%)) 12%
    );
  }
}
.overlay-link-hover-subtle:focus-visible {
  color: color-mix(
    in oklch,
    oklch(var(--content-on-surface-link, var(--link))) 88%,
    oklch(var(--overlay-hover-subtle, 0% 0 0 / 20%)) 12%
  );
}
@media (prefers-reduced-motion: reduce) {
  .overlay-link-hover-subtle {
    transition: none;
  }
}
`.trim();

/**
 * UnoCSS preset with utility rules required by the UI library's components.
 * Adds Tailwind v4 CSS variable syntax (w-(--var), h-(--var), etc.)
 * that preset-wind4 does not yet support.
 * @returns {import('unocss').Preset} UnoCSS utility preset for ui-library components.
 */
export const presetUiLibraryUtils = () =>
  definePreset(() => ({
    name: '@aleph-alpha/ui-library-utils',
    preflights: [{ getCSS: () => vHoverOverlayCSS }],
    rules: [
      [/^w-\(--(.+)\)$/, ([, name]) => ({ width: `var(--${name})` })],
      [/^max-w-\(--(.+)\)$/, ([, name]) => ({ 'max-width': `var(--${name})` })],
      [/^h-\(--(.+)\)$/, ([, name]) => ({ height: `var(--${name})` })],
      [
        /^origin-\(--(.+)\)$/,
        ([, name]) => ({ 'transform-origin': `var(--${name})` }),
      ],
      [
        /^bg-overlay-hover-(subtle|default|strong)$/,
        ([, overlayVariant]) => ({
          'background-color': `oklch(var(--overlay-hover-${overlayVariant}))`,
        }),
      ],
      [
        /^bg-background-surface-muted-subtle$/,
        () => ({
          'background-color': 'oklch(var(--background-surface-muted-subtle))',
        }),
      ],
    ],
  }))();

/**
 * Creates a complete UnoCSS config with all required presets, content scanning,
 * font loading, and theme from design tokens. Drop-in replacement for manually
 * composing presetWind, presetAnimations, presetShadcn, and the ui-library presets.
 * @example
 * ```ts
 * // uno.config.ts — minimal
 * import { defineUiLibraryConfig } from '@aleph-alpha/ui-library/config'
 * export default defineUiLibraryConfig()
 *
 * // uno.config.ts — with customization
 * import { defineUiLibraryConfig } from '@aleph-alpha/ui-library/config'
 * export default defineUiLibraryConfig({
 *   extend: { light: { primary: '#ff5500' } },
 *   darkSelector: 'html.dark',
 *   rootFontSize: '100%',
 * })
 * ```
 * @param {object} [options] Ui-library config options.
 * @returns {import('unocss').UserConfig} Complete UnoCSS config.
 */
export const defineUiLibraryConfig = (options = {}) => {
  const {
    darkSelector,
    rootSelector,
    rootFontSize,
    extend,
    theme,
    includeAllTokens,
    themeMode,
    accessible,
    accessibleTheme,
    accessibleSelector,
    presets: extraPresets = [],
    rules: extraRules = [],
    shortcuts: extraShortcuts = [],
    ...restConfig
  } = options;

  const themeOptions = {};
  if (darkSelector !== undefined) themeOptions.darkSelector = darkSelector;
  if (rootSelector !== undefined) themeOptions.rootSelector = rootSelector;
  if (rootFontSize !== undefined) themeOptions.rootFontSize = rootFontSize;
  if (extend !== undefined) themeOptions.extend = extend;
  if (theme !== undefined) themeOptions.theme = theme;
  if (includeAllTokens !== undefined)
    themeOptions.includeAllTokens = includeAllTokens;
  if (themeMode !== undefined) themeOptions.themeMode = themeMode;
  if (accessible !== undefined) themeOptions.accessible = accessible;
  if (accessibleTheme !== undefined)
    themeOptions.accessibleTheme = accessibleTheme;
  if (accessibleSelector !== undefined)
    themeOptions.accessibleSelector = accessibleSelector;

  return defineConfig({
    ...getUiLibraryContentConfig(),
    safelist: ['overlay-link-hover-subtle'],
    presets: [
      presetWind(),
      presetAnimations(),
      presetShadcn({
        color: 'neutral',
        darkSelector: darkSelector ?? 'html.dark',
      }),
      // Spark Colors / Spacings / Box-shadow tokens. Typography utilities are
      // filtered out where ui-library owns the same class names.
      getSparkTokenPresetWithoutUiLibraryTypography(),
      presetUiLibraryThemeFromTokens(themeOptions),
      presetUiLibraryUtils(),
      ...extraPresets,
    ],
    rules: [...extraRules],
    shortcuts: [...extraShortcuts],
    ...restConfig,
  });
};

// Typography utilities — font families resolve from `2-theme/AA Base`.
// Token variants whose name includes `mono` are omitted (no `label-*-mono-*` utilities).
const fontWeightMap = {
  Regular: '400',
  Medium: '500',
  'Semi Bold': '600',
  SemiBold: '600',
  Bold: '700',
  Light: '300',
};

/** Design-tool sizes are px; rem token values resolve against a 16px base. */
const ROOT_FONT_SIZE_PX = 16;

const formatPx = (value) => {
  const rounded = Math.round(value * 10000) / 10000;
  return `${rounded}px`;
};

const numericDesignPxToPx = (raw) => {
  const n = typeof raw === 'number' ? raw : parseFloat(String(raw).trim());
  if (!Number.isFinite(n)) return null;
  return formatPx(n);
};

const numericDesignPxToRem = (raw) => {
  const n = typeof raw === 'number' ? raw : parseFloat(String(raw).trim());
  if (!Number.isFinite(n)) return null;
  const rem = n / ROOT_FONT_SIZE_PX;
  const rounded = Math.round(rem * 10000) / 10000;
  return `${rounded}rem`;
};

const remToPx = (raw) => {
  const n = parseFloat(String(raw).trim());
  if (!Number.isFinite(n)) return null;
  return formatPx(n * ROOT_FONT_SIZE_PX);
};

/**
 * Token Studio may export dimensions as strings; font-size output is always px.
 * @param {string | number | null | undefined} value Raw token font-size value.
 * @returns {string | number | null | undefined} CSS-ready font-size value.
 */
const normalizeFontSizeForCss = (value) => {
  if (value == null) return value;
  if (typeof value === 'number' && Number.isFinite(value))
    return numericDesignPxToPx(value);
  if (typeof value === 'string') {
    const t = value.trim();
    if (/^(calc|var|--)/i.test(t)) return t;
    if (/^-?\d+(\.\d+)?rem$/i.test(t)) return remToPx(t) ?? t;
    if (/^-?\d+(\.\d+)?px$/i.test(t)) return t;
    if (/[a-z%]/i.test(t)) return t;
    if (/^\d+(\.\d+)?$/.test(t)) return numericDesignPxToPx(t) ?? t;
  }
  return value;
};

/**
 * Line heights: pixel-like values → rem; small integers (0–3) stay unitless multipliers;
 * decimals in (0, 3] stay unitless ratios (e.g. "1.5").
 * @param {string | number | null | undefined} value Raw token line-height value.
 * @returns {string | number | null | undefined} CSS-ready line-height value.
 */
const normalizeLineHeightForCss = (value) => {
  if (value == null) return value;
  if (typeof value === 'number' && Number.isFinite(value)) {
    if (value >= 0 && value < 4 && Number.isInteger(value))
      return String(value);
    if (value > 0 && value <= 3 && !Number.isInteger(value))
      return String(value);
    return numericDesignPxToRem(value) ?? String(value);
  }
  if (typeof value === 'string') {
    const t = value.trim();
    if (/^(calc|var|--)/i.test(t)) return t;
    if (/[a-z%]/i.test(t)) return t;
    if (/^\d+\.\d+$/.test(t)) {
      const n = parseFloat(t);
      if (n > 0 && n <= 3) return t;
      return numericDesignPxToRem(t) ?? t;
    }
    if (/^\d+$/.test(t)) {
      const n = parseInt(t, 10);
      if (n >= 0 && n < 4) return t;
      return numericDesignPxToRem(t) ?? t;
    }
  }
  return value;
};

const getAaBaseFontFamilyName = (fontFamilyRef) => {
  const refPath =
    typeof fontFamilyRef === 'string'
      ? fontFamilyRef.match(/^\{(.+)\}$/)?.[1]?.trim()
      : null;
  const tokenKey = refPath?.startsWith('font.font-family.')
    ? refPath.slice('font.font-family.'.length)
    : 'font-body';
  const candidateKeys =
    {
      body: ['body', 'font-body'],
      heading: ['heading', 'font-heading'],
      mono: ['mono', 'font-mono'],
      'font-body': ['font-body', 'body'],
      'font-heading': ['font-heading', 'heading'],
      'font-mono': ['font-mono', 'mono'],
    }[tokenKey] ?? [tokenKey];

  for (const candidateKey of candidateKeys) {
    const name = resolveTokenValue(
      UI_LIBRARY_REFERENCE_THEME_SETS.default,
      `font.font-family.${candidateKey}`,
    );
    const trimmed = name != null ? String(name).trim() : '';
    if (trimmed) return trimmed;
  }

  return 'Saans';
};

const isTypographyTokenGroup = (group) =>
  group &&
  typeof group === 'object' &&
  Object.values(group).some(
    (v) =>
      v && typeof v === 'object' && v.type === 'typography' && v.value != null,
  );

/**
 * Scale keys named `Heading …` map to `heading-{scale}` (no weight suffix): `heading-s`, `heading-m`, … `heading-2xl`.
 * @param {string} tokenKey Typography token group name.
 * @returns {string} Utility class group prefix.
 */
const groupKebabForTypographyKey = (tokenKey) => {
  const trimmed = tokenKey.trim();
  if (/^Heading\s+/i.test(trimmed)) {
    const rest = trimmed
      .replace(/^Heading\s+/i, '')
      .trim()
      .replace(/\s+/g, '-')
      .toLowerCase();
    if (!rest) return 'heading-s';
    return `heading-${rest}`;
  }
  return trimmed.replace(/\s+/g, '-').toLowerCase();
};

/**
 * Resolves composite typography; `{…}` refs use the token search order (AA Base wins over Solea/global).
 * `fontFamily` output is resolved from `2-theme/AA Base`.
 * @param {object} typographyValue Composite typography token value.
 * @returns {{ fontFamily?: string, fontSize?: string | number, fontWeight?: string | number, lineHeight?: string | number }} Resolved CSS typography properties.
 */
const resolveTypographyToken = (typographyValue) => {
  const resolved = {};

  if (typographyValue.fontWeight) {
    const weight = resolveReferences(typographyValue.fontWeight, new Set());
    resolved.fontWeight = fontWeightMap[weight] || weight;
  }

  if (typographyValue.fontSize) {
    const size = resolveReferences(typographyValue.fontSize, new Set());
    resolved.fontSize = normalizeFontSizeForCss(size);
  }

  if (typographyValue.lineHeight) {
    const lineHeight = resolveReferences(typographyValue.lineHeight, new Set());
    resolved.lineHeight = normalizeLineHeightForCss(lineHeight);
  }

  resolved.fontFamily = getAaBaseFontFamilyName(typographyValue.fontFamily);

  return resolved;
};

const setResolvedTypographyShortcut = (shortcuts, className, resolved) => {
  const cssProps = [];
  if (resolved.fontFamily) cssProps.push(`font-family:${resolved.fontFamily}`);
  if (resolved.fontSize) cssProps.push(`font-size:${resolved.fontSize}`);
  if (resolved.fontWeight) cssProps.push(`font-weight:${resolved.fontWeight}`);
  if (resolved.lineHeight) cssProps.push(`line-height:${resolved.lineHeight}`);
  if (cssProps.length === 0) return;
  const css = cssProps.join(';');
  shortcuts.set(className, css);
};

/**
 * Get typography shortcuts for UnoCSS.
 * Scans `global` for groups whose children are typography tokens (e.g. `Label 12`, `Body 14`, `Heading S`).
 * Headings use one no-suffix utility per token group (`heading-s` … `heading-2xl`).
 * Font family resolves from `2-theme/AA Base`.
 * @example
 * - 'label-12-medium', 'heading-s', 'heading-m', 'heading-2xl'
 * @returns {Array<[string, string]>} Typography shortcut tuples for UnoCSS rules.
 */
export const getUiLibraryTypographyShortcuts = () => {
  const global = getTokens()?.global;
  if (!global) return [];

  const shortcuts = new Map();

  for (const [tokenKey, tokenGroup] of Object.entries(global)) {
    if (!isTypographyTokenGroup(tokenGroup)) continue;

    const groupKebab = groupKebabForTypographyKey(tokenKey);
    const isHeadingGroup = /^Heading\s+/i.test(tokenKey.trim());

    for (const [variant, tokenData] of Object.entries(tokenGroup)) {
      if (!tokenData?.value || tokenData.type !== 'typography') continue;

      const variantKebab = variant.toLowerCase().replace(/\s+/g, '-');
      if (variantKebab.includes('mono')) continue;

      const resolved = resolveTypographyToken(tokenData.value);
      const className = isHeadingGroup
        ? groupKebab
        : `${groupKebab}-${variantKebab}`;

      setResolvedTypographyShortcut(shortcuts, className, resolved);
      // Token name is `regular`; many components use `*-normal` — keep both class names.
      if (!isHeadingGroup && variantKebab === 'regular') {
        setResolvedTypographyShortcut(
          shortcuts,
          `${groupKebab}-normal`,
          resolved,
        );
      }
    }
  }

  return Array.from(shortcuts);
};

export const getSparkTokenPresetWithoutUiLibraryTypography = () => {
  const uiLibraryTypographyClassNames = new Set(
    getUiLibraryTypographyShortcuts().map(([className]) => className),
  );

  return {
    ...sparkTokenPreset,
    rules: (sparkTokenPreset.rules ?? []).filter((rule) => {
      const matcher = rule?.[0];
      return (
        typeof matcher !== 'string' ||
        !uiLibraryTypographyClassNames.has(matcher)
      );
    }),
  };
};
