import { parseHex, type Rgb } from './color'
import type { ModePair } from './types'
import type { Ramp } from './ramps'

// Canonical OKLab (Björn Ottosson). sRGB → linear-RGB → LMS → cbrt → Lab → LCh.
// NEVER the CIE XYZ/Lab path — OKLab is perceptually uniform; CIELAB is not, and the
// two diverge sharply (e.g. #0000ff: OKLab L 0.452 vs CIELAB L*/100 0.323).

function srgbToLinear(c: number): number {
  const x = c / 255
  return x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
}

function linearToByte(c: number): number {
  const x = Math.min(1, Math.max(0, c))
  const v = x <= 0.0031308 ? 12.92 * x : 1.055 * Math.pow(x, 1 / 2.4) - 0.055
  return Math.round(Math.min(1, Math.max(0, v)) * 255)
}

function toHex(rgb: Rgb): string {
  return '#' + rgb.map((x) => x.toString(16).padStart(2, '0')).join('')
}

export interface Oklch {
  L: number
  C: number
  H: number
}

export function srgbToOklch(hex: string): Oklch | null {
  const rgb = parseHex(hex)
  if (!rgb) return null
  const r = srgbToLinear(rgb[0]), g = srgbToLinear(rgb[1]), b = srgbToLinear(rgb[2])
  const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b)
  const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b)
  const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b)
  const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s
  const a = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s
  const bb = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s
  let H = (Math.atan2(bb, a) * 180) / Math.PI
  if (H < 0) H += 360
  return { L, C: Math.hypot(a, bb), H }
}

export function oklchToHex(L: number, C: number, H: number): string {
  const hr = (H * Math.PI) / 180
  const a = C * Math.cos(hr), b = C * Math.sin(hr)
  const l_ = L + 0.3963377774 * a + 0.2158037573 * b
  const m_ = L - 0.1055613458 * a - 0.0638541728 * b
  const s_ = L - 0.0894841775 * a - 1.291485548 * b
  const l = l_ ** 3, m = m_ ** 3, s = s_ ** 3
  const r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
  const g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
  const bl = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s
  return toHex([linearToByte(r), linearToByte(g), linearToByte(bl)])
}

// Radix-calibrated OKLab L target curves (the generator drives L monotonically; the
// seed contributes HUE, not L — guarantees a monotonic perceptual ramp by construction).
const LIGHT_L = [0.985, 0.96, 0.93, 0.895, 0.855, 0.805, 0.745, 0.67, 0.585, 0.5, 0.41, 0.31] as const
const DARK_L = [0.18, 0.215, 0.255, 0.3, 0.35, 0.405, 0.47, 0.545, 0.585, 0.66, 0.8, 0.93] as const
// Chroma curve peaks at the solid-fill step (index 8 = step 9).
const CHROMA = [0.01, 0.02, 0.035, 0.055, 0.075, 0.095, 0.115, 0.135, 0.15, 0.135, 0.11, 0.08] as const
const CHROMA_SCALE: Record<'neutral' | 'accent' | 'intent', number> = {
  neutral: 0.12,
  accent: 1.0,
  intent: 1.0,
}

// WCAG relative luminance + contrast — for best-polarity `contrast` selection.
function relLuminance(rgb: Rgb): number {
  const f = (v: number): number => {
    const x = v / 255
    return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4)
  }
  return 0.2126 * f(rgb[0]) + 0.7152 * f(rgb[1]) + 0.0722 * f(rgb[2])
}
function contrast(aHex: string, bHex: string): number {
  const la = relLuminance(parseHex(aHex)!), lb = relLuminance(parseHex(bHex)!)
  return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05)
}

// Radix-style alpha derivation: express `target` as a color over `bg` at minimal alpha.
function alphaOver(target: Rgb, bg: Rgb): string {
  const need = [0, 1, 2].map((i) => {
    const t = target[i]!, g = bg[i]!
    const fg = t <= g ? 0 : 255
    return fg === g ? 0 : (t - g) / (fg - g)
  })
  const alpha = Math.max(need[0]!, need[1]!, need[2]!, 0.0001)
  const c = [0, 1, 2].map((i) => {
    const g = bg[i]!, t = target[i]!
    return Math.min(255, Math.max(0, Math.round(g + (t - g) / alpha)))
  })
  return `rgba(${c[0]}, ${c[1]}, ${c[2]}, ${Math.min(1, alpha).toFixed(4)})`
}

function buildRamp(hue: number, Larr: readonly number[], scale: number): Ramp {
  const steps = Larr.map((L, i) => oklchToHex(L, CHROMA[i]! * scale, hue))
  const bg = parseHex(steps[0]!)!
  const alpha = steps.map((s) => alphaOver(parseHex(s)!, bg))
  const fill = steps[8]!
  const contrastHex = contrast('#ffffff', fill) >= contrast('#000000', fill) ? '#ffffff' : '#000000'
  return { steps, alpha, contrast: contrastHex }
}

export function generateRamp(
  seedHex: string,
  role: 'neutral' | 'accent' | 'intent',
): ModePair<Ramp> | null {
  const seed = srgbToOklch(seedHex)
  if (!seed) return null
  const scale = CHROMA_SCALE[role]
  return {
    light: buildRamp(seed.H, LIGHT_L, scale),
    dark: buildRamp(seed.H, DARK_L, scale),
  }
}
