import type {
  PaletteSet, Palette, ColorTokens, ContrastPair, ContrastReport, ContrastFailure,
  ContrastTokenReport, ContrastPairFailure, Result,
} from './types'
import { ThemeValidationError, ThemeColorFormatError, type ThemeError } from './errors'
import { COLOR_TOKEN_KEYS, SHAPE_TOKEN_KEYS } from './keys'
import { CONTRAST_PAIRS } from './contrast-pairs'
import { parseHex, contrastRatio } from './color'

const KEBAB = /^[a-z0-9]+(-[a-z0-9]+)*$/
const isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null
const isStr = (v: unknown): v is string => typeof v === 'string'
const required = (level: ContrastPair['level']): number => (level === 'text' ? 4.5 : 3.0)

export function validatePaletteSet(pkg: unknown): Result<PaletteSet, ThemeError[]> {
  const errs: ThemeError[] = []
  if (!isObj(pkg)) return { ok: false, error: [new ThemeValidationError('package', 'must be an object')] }

  if (!isStr(pkg.id) || !KEBAB.test(pkg.id)) errs.push(new ThemeValidationError('id', 'missing or not kebab-case'))
  if (!isStr(pkg.name)) errs.push(new ThemeValidationError('name', 'missing'))

  const palettes = pkg.palettes
  if (!Array.isArray(palettes) || palettes.length < 1) {
    errs.push(new ThemeValidationError('palettes', 'must be a non-empty array'))
  } else {
    const ids = new Set<string>()
    palettes.forEach((p, i) => {
      if (!isObj(p)) { errs.push(new ThemeValidationError(`palettes[${i}]`, 'must be an object')); return }
      if (!isStr(p.id) || !KEBAB.test(p.id)) errs.push(new ThemeValidationError(`palettes[${i}].id`, 'missing or not kebab-case'))
      else { if (ids.has(p.id)) errs.push(new ThemeValidationError(`palettes[${i}].id`, `duplicate: ${p.id}`)); ids.add(p.id) }
      if (!isStr(p.name)) errs.push(new ThemeValidationError(`palettes[${i}].name`, 'missing'))
      const colors = (p as { colors?: unknown }).colors
      if (!isObj(colors) || !isObj(colors.light) || !isObj(colors.dark)) {
        errs.push(new ThemeValidationError(`palettes[${i}].colors`, 'must have light + dark token maps'))
      } else {
        for (const mode of ['light', 'dark'] as const) {
          const map = colors[mode] as Record<string, unknown>
          for (const key of COLOR_TOKEN_KEYS) {
            const val = map[key]
            if (!isStr(val)) errs.push(new ThemeValidationError(`palettes[${i}].colors.${mode}.${key}`, 'missing'))
            else if (!parseHex(val)) errs.push(new ThemeColorFormatError(`palettes[${i}].colors.${mode}.${key}`, val))
          }
        }
      }
    })
    if (isStr(pkg.defaultPaletteId) && !ids.has(pkg.defaultPaletteId)) {
      errs.push(new ThemeValidationError('defaultPaletteId', `does not resolve: ${pkg.defaultPaletteId}`))
    }
  }
  if (!isStr(pkg.defaultPaletteId)) errs.push(new ThemeValidationError('defaultPaletteId', 'missing'))

  const shape = pkg.shape
  if (!isObj(shape) || !isObj(shape.light) || !isObj(shape.dark)) {
    errs.push(new ThemeValidationError('shape', 'must have light + dark token maps'))
  } else {
    for (const mode of ['light', 'dark'] as const) {
      const map = shape[mode] as Record<string, unknown>
      for (const key of SHAPE_TOKEN_KEYS) {
        if (!isStr(map[key])) errs.push(new ThemeValidationError(`shape.${mode}.${key}`, 'missing'))
      }
    }
  }

  return errs.length ? { ok: false, error: errs } : { ok: true, value: pkg as unknown as PaletteSet }
}

/**
 * SINGLE-MODE Hard-floor checker — the shared impl + the override-persist seam.
 * Accepts any ColorTokens (an EffectiveTokens override result is a superset; extra shape keys ignored).
 */
export function validateContrastTokens(
  colors: ColorTokens,
  pairs: readonly ContrastPair[] = CONTRAST_PAIRS,
): ContrastTokenReport {
  const failures: ContrastPairFailure[] = []
  for (const pair of pairs) {
    const need = required(pair.level)
    const fgRgb = parseHex(colors[pair.fg] ?? '')
    const bgRgb = parseHex(colors[pair.bg] ?? '')
    // FAIL-CLOSED: an unparseable color is a failure with ratio 0 — never skipped, never passed.
    const ratio = fgRgb && bgRgb ? contrastRatio(fgRgb, bgRgb) : 0
    if (ratio < need) failures.push({ fg: pair.fg, bg: pair.bg, ratio, required: need })
  }
  return { pass: failures.length === 0, failures }
}

export function validateContrast(
  pkg: PaletteSet,
  pairs: readonly ContrastPair[] = CONTRAST_PAIRS,
): ContrastReport {
  const failures: ContrastFailure[] = []
  for (const palette of pkg.palettes) {
    for (const mode of ['light', 'dark'] as const) {
      const map = (palette as Palette).colors[mode]
      // Same WCAG math + pairs as the single-mode checker — tag each failure with its coordinates.
      for (const f of validateContrastTokens(map, pairs).failures) {
        failures.push({ ...f, paletteId: palette.id, mode })
      }
    }
  }
  return { pass: failures.length === 0, failures }
}
