import type { ContrastCheck, ContrastReport, ThemeProposal } from './types.js';

/** Parse `#rgb` / `#rrggbb` → `{ r, g, b }` 0..255. Throws on invalid. */
export function parseHex(hex: string): { r: number; g: number; b: number } {
  const m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(hex.trim());
  if (!m) throw new Error(`Invalid hex colour: ${hex}`);
  let body = m[1]!;
  if (body.length === 3) {
    body = body.split('').map((ch) => ch + ch).join('');
  }
  const num = parseInt(body, 16);
  return {
    r: (num >> 16) & 0xff,
    g: (num >> 8) & 0xff,
    b: num & 0xff,
  };
}

/** Render `{ r, g, b }` → `#rrggbb`. */
export function toHex(r: number, g: number, b: number): string {
  const c = (v: number) => Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, '0');
  return `#${c(r)}${c(g)}${c(b)}`;
}

/** Relative luminance per WCAG 2.x. */
export function relativeLuminance(hex: string): number {
  const { r, g, b } = parseHex(hex);
  const f = (v: number) => {
    const s = v / 255;
    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
  };
  return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
}

/** WCAG contrast ratio for two hex colours. */
export function contrast(a: string, b: string): number {
  const la = relativeLuminance(a);
  const lb = relativeLuminance(b);
  const lo = Math.min(la, lb);
  const hi = Math.max(la, lb);
  return (hi + 0.05) / (lo + 0.05);
}

export function report(fg: string, bg: string): ContrastReport {
  const ratio = contrast(fg, bg);
  return { ratio, aa: ratio >= 4.5, aaa: ratio >= 7 };
}

/** Audit the canonical text/accent pairings in a theme proposal. */
export function auditTheme(theme: ThemeProposal): ContrastCheck {
  return {
    textPrimaryOnSurface: report(theme.textPrimary, theme.surface),
    textSecondaryOnSurface: report(theme.textSecondary, theme.surface),
    textPrimaryOnSurfaceCard: report(theme.textPrimary, theme.surfaceCard),
    accentOnSurface: report(theme.accent, theme.surface),
  };
}

/**
 * Iteratively darken or lighten `fg` until `contrast(fg, bg) >=
 * target`. Caps at black / white. Pure function.
 */
export function adjustForContrast(fg: string, bg: string, target: number): string {
  let current = fg;
  // Decide direction once: if bg is light, push fg darker; else lighter.
  const bgLum = relativeLuminance(bg);
  const darker = bgLum > 0.5;
  for (let i = 0; i < 30; i++) {
    if (contrast(current, bg) >= target) return current;
    current = shift(current, darker ? -0.04 : 0.04);
  }
  return current;
}

function shift(hex: string, delta: number): string {
  const { r, g, b } = parseHex(hex);
  const k = delta * 255;
  return toHex(r + k, g + k, b + k);
}

/**
 * Enforce a contrast policy across the audited pairings, returning a
 * new ThemeProposal where any failing fg has been nudged toward black /
 * white until it passes.
 */
export function enforceContrast(
  theme: ThemeProposal,
  policy: 'aa' | 'aaa' = 'aa',
): ThemeProposal {
  const target = policy === 'aaa' ? 7 : 4.5;
  return {
    ...theme,
    textPrimary: adjustForContrast(theme.textPrimary, theme.surface, target),
    textSecondary: adjustForContrast(theme.textSecondary, theme.surface, Math.max(3, target - 1.5)),
    accent: adjustForContrast(theme.accent, theme.surface, Math.max(3, target - 1.5)),
  };
}
