import type { FieldDefinition, FieldValue, FieldValueMap, MediaRef, RelRef, ResolvedFieldGroup } from './model.js'
import { isMultipleCapable } from './model.js'
import { FieldValidationError } from './errors.js'

const COLOR_RE = /^#([0-9a-f]{6}|[0-9a-f]{8})$/i
const EMAIL_RE = /^[^@\s]+@[^@\s]+\.[^@\s]+$/

function isValidUrl(s: string): boolean {
  try {
    const u = new URL(s)
    return u.protocol === 'http:' || u.protocol === 'https:'
  } catch {
    return false
  }
}

function isMediaRef(v: unknown): v is MediaRef {
  return typeof v === 'object' && v !== null && typeof (v as MediaRef).key === 'string' && (v as MediaRef).key.length > 0
}

function isRelRef(v: unknown): v is RelRef {
  return (
    typeof v === 'object' &&
    v !== null &&
    typeof (v as RelRef).entityType === 'string' &&
    typeof (v as RelRef).entityId === 'string'
  )
}

/** Pure, validator-agnostic. Throws FieldValidationError on first offending field.
 *  Does NOT check relationship existence (that is setEntityValues' optional async resolver, §6). */
export function validateValues(group: ResolvedFieldGroup, values: FieldValueMap): void {
  const defs = new Map(group.fields.map((f) => [f.key, f]))
  for (const k of Object.keys(values)) {
    if (!defs.has(k)) throw new FieldValidationError(k, 'not defined in group')
  }
  for (const f of group.fields) validateField(f, values[f.key])
}

function validateField(f: FieldDefinition, v: FieldValue | undefined): void {
  if (v === undefined || v === null) {
    if (f.required) throw new FieldValidationError(f.key, 'required')
    return
  }
  const multiple = 'multiple' in f && f.multiple === true
  if (multiple) {
    if (!Array.isArray(v)) throw new FieldValidationError(f.key, 'must be an array (multiple)')
    for (const item of v) validateScalar(f, item)
    return
  }
  if (Array.isArray(v) && !isMultipleCapable(f.type)) {
    throw new FieldValidationError(f.key, 'array not allowed for this type')
  }
  validateScalar(f, v as never)
}

function validateScalar(f: FieldDefinition, v: FieldValue): void {
  switch (f.type) {
    case 'text':
    case 'textarea': {
      if (typeof v !== 'string') throw new FieldValidationError(f.key, 'must be a string')
      if (f.maxLength !== undefined && v.length > f.maxLength) {
        throw new FieldValidationError(f.key, `exceeds maxLength ${f.maxLength}`)
      }
      if (f.type === 'text' && f.pattern !== undefined) {
        try {
          if (!new RegExp(f.pattern).test(v)) throw new FieldValidationError(f.key, 'pattern mismatch')
        } catch {
          throw new FieldValidationError(f.key, 'pattern mismatch')
        }
      }
      break
    }
    case 'number': {
      if (typeof v !== 'number' || Number.isNaN(v)) throw new FieldValidationError(f.key, 'must be a number')
      if (f.integer && !Number.isInteger(v)) throw new FieldValidationError(f.key, 'must be an integer')
      if (f.min !== undefined && v < f.min) throw new FieldValidationError(f.key, `below min ${f.min}`)
      if (f.max !== undefined && v > f.max) throw new FieldValidationError(f.key, `above max ${f.max}`)
      break
    }
    case 'boolean': {
      if (typeof v !== 'boolean') throw new FieldValidationError(f.key, 'must be a boolean')
      break
    }
    case 'select': {
      if (typeof v !== 'string') throw new FieldValidationError(f.key, 'must be a string')
      const allowed = new Set(f.options.map((o) => o.value))
      if (!allowed.has(v)) throw new FieldValidationError(f.key, 'value not in options')
      break
    }
    case 'date': {
      const d = v instanceof Date ? v : new Date(v as string)
      if (Number.isNaN(d.getTime())) throw new FieldValidationError(f.key, 'must be a parseable date')
      break
    }
    case 'color': {
      if (typeof v !== 'string' || !COLOR_RE.test(v)) throw new FieldValidationError(f.key, 'invalid color format')
      break
    }
    case 'url': {
      if (typeof v !== 'string' || !isValidUrl(v)) throw new FieldValidationError(f.key, 'invalid url')
      break
    }
    case 'email': {
      if (typeof v !== 'string' || !EMAIL_RE.test(v)) throw new FieldValidationError(f.key, 'invalid email')
      break
    }
    case 'media': {
      if (!isMediaRef(v)) throw new FieldValidationError(f.key, 'media requires key')
      break
    }
    case 'relationship': {
      if (!isRelRef(v)) throw new FieldValidationError(f.key, 'invalid relationship ref')
      if (v.entityType !== f.targetEntityType) {
        throw new FieldValidationError(f.key, 'targetEntityType mismatch')
      }
      break
    }
  }
}
