// Framework-neutral form validators. ZERO Vue / component imports — this module
// is TDD'd headless and reused by the React binding (硬规则 #8 dual-framework parity).

export type FormTrigger = 'blur' | 'change'

export interface FormRule {
  required?: boolean
  type?: 'string' | 'number' | 'integer' | 'email' | 'url'
  min?: number // number domain: value floor; string: length floor
  max?: number // number domain: value ceiling; string: length ceiling
  len?: number // exact length (string) / exact value (number)
  pattern?: RegExp
  validator?: (value: unknown, model: Record<string, unknown>) => true | string // sync only
  message?: string // overrides the default message for whichever check fails
  trigger?: FormTrigger | FormTrigger[] // default 'change' (applied by the engine)
}

export type FormRules = Record<string, FormRule | FormRule[]>

const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/

export function normalizeRules(rules: FormRule | FormRule[] | undefined): FormRule[] {
  if (!rules) return []
  return Array.isArray(rules) ? rules : [rules]
}

function isEmpty(value: unknown): boolean {
  return value === undefined || value === null || value === ''
}

// number → the value itself; string/array → its length; else 0.
function measure(value: unknown): number {
  if (typeof value === 'number') return value
  if (typeof value === 'string' || Array.isArray(value)) return value.length
  return 0
}

function checkType(value: unknown, type: NonNullable<FormRule['type']>): boolean {
  switch (type) {
    case 'string': return typeof value === 'string'
    case 'number': return typeof value === 'number' && !Number.isNaN(value)
    case 'integer': return typeof value === 'number' && Number.isInteger(value)
    case 'email': return typeof value === 'string' && EMAIL_RE.test(value)
    case 'url':
      try { new URL(String(value)); return true } catch { return false }
  }
}

const TYPE_MESSAGE: Record<NonNullable<FormRule['type']>, string> = {
  string: 'Please enter valid text',
  number: 'Please enter a valid number',
  integer: 'Please enter a valid integer',
  email: 'Please enter a valid email',
  url: 'Please enter a valid URL',
}

// Runs one rule; '' = pass, else the (custom or default) error message.
function runRule(value: unknown, rule: FormRule, model: Record<string, unknown>): string {
  if (rule.required && isEmpty(value)) return rule.message || 'This field is required'
  // A non-required empty value skips every remaining check (el-form behaviour).
  if (isEmpty(value)) return ''

  if (rule.type && !checkType(value, rule.type)) return rule.message || TYPE_MESSAGE[rule.type]

  const isNum = typeof value === 'number'
  if (rule.len != null && measure(value) !== rule.len) {
    return rule.message || (isNum ? `Must equal ${rule.len}` : `Must be exactly ${rule.len} characters`)
  }
  if (rule.min != null && measure(value) < rule.min) {
    return rule.message || (isNum ? `Must be at least ${rule.min}` : `Must be at least ${rule.min} characters`)
  }
  if (rule.max != null && measure(value) > rule.max) {
    return rule.message || (isNum ? `Must be at most ${rule.max}` : `Must be at most ${rule.max} characters`)
  }
  if (rule.pattern && !rule.pattern.test(String(value))) return rule.message || 'Invalid format'
  if (rule.validator) {
    const result = rule.validator(value, model)
    if (result !== true) return typeof result === 'string' ? result : (rule.message || 'Invalid value')
  }
  return ''
}

// Runs rules in order, short-circuiting on the first failure. '' = all pass.
export function runRules(value: unknown, rules: FormRule[], model: Record<string, unknown>): string {
  for (const rule of rules) {
    const error = runRule(value, rule, model)
    if (error) return error
  }
  return ''
}
