// Internal WCAG-2.x contrast math. PINNED — do not "optimize" the constants.
// Spec §2b: hex-only canonical input; non-hex => null (fail-closed at the caller).

export type Rgb = [number, number, number]

const HEX_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i

/** Parse `#rgb` / `#rrggbb` (case-insensitive, trimmed) to [r,g,b] 0..255, or null. */
export function parseHex(hex: string): Rgb | null {
  const m = HEX_RE.exec(hex.trim())
  if (!m) return null
  const h = m[1]
  if (!h) return null
  if (h.length === 3) {
    return [
      parseInt(h[0]! + h[0]!, 16),
      parseInt(h[1]! + h[1]!, 16),
      parseInt(h[2]! + h[2]!, 16),
    ]
  }
  return [
    parseInt(h.slice(0, 2), 16),
    parseInt(h.slice(2, 4), 16),
    parseInt(h.slice(4, 6), 16),
  ]
}

/** WCAG-2.x relative luminance (sRGB). white -> 1, black -> 0. */
export function relativeLuminance([r, g, b]: Rgb): number {
  const lin = (c: number): number => {
    const s = c / 255
    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
  }
  return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
}

/** WCAG-2.x contrast ratio. Order-independent (auto lighter/darker). Range 1..21. */
export function contrastRatio(a: Rgb, b: Rgb): number {
  const la = relativeLuminance(a)
  const lb = relativeLuminance(b)
  const lighter = Math.max(la, lb)
  const darker = Math.min(la, lb)
  return (lighter + 0.05) / (darker + 0.05)
}
