// Framework-neutral form engine. Depends ONLY on validators.ts — no Vue, no
// components. Cross-CE reactivity is handled by explicit subscribe(), NOT Vue
// reactivity (Vue's forwarding chain arrives too late under defineCustomElement;
// see src/canonical/composables/useHasSlot.ts).
// 设计见 docs/superpowers/specs/2026-07-21-form-validation-engine-design.md
//（§3 三层解耦 + 为什么引擎核用显式 subscribe() · §1 二值 pass/fail 的态范围是 owner
//  拍板、不是省事 · §6 CE 跨边界 accessor 是最高风险块，改这块前先看那里的 de-risk 结论）
import { normalizeRules, runRules, type FormRule, type FormRules, type FormTrigger } from './validators'

export interface FormEngine {
  registerField(prop: string, getRules: () => FormRule[]): void
  unregisterField(prop: string): void
  validate(): Promise<{ valid: boolean; errors: Record<string, string> }>
  validateField(prop: string, trigger?: FormTrigger): Promise<string>
  resetFields(): void
  clearValidate(props?: string | string[]): void
  subscribe(listener: (errors: Record<string, string>) => void): () => void
  getFieldValue(prop: string): unknown
  getError(prop: string): string
  getDisabled(): boolean
}

function ruleMatchesTrigger(rule: FormRule, trigger: FormTrigger): boolean {
  const t = rule.trigger ?? 'change'
  return Array.isArray(t) ? t.includes(trigger) : t === trigger
}

// Deep-clones the initial model so resetFields() can restore it. JSON clone is
// sufficient for the flat/serialisable models this MVP targets. Null-safe: under
// the React/CE binding the `model` element property is set asynchronously (useEffect
// after first mount), so getModel() can be undefined during Form's initial setup —
// treat that as an empty model rather than throwing (which would crash the CE render).
function snapshot(model: Record<string, unknown> | undefined | null): Record<string, unknown> {
  if (model == null) return {}
  return JSON.parse(JSON.stringify(model))
}

export function createFormEngine(opts: {
  getModel: () => Record<string, unknown>
  getFormRules: () => FormRules
  // INFRA-F120: Form's `disabled` prop, read lazily so cross-component reactivity
  // works the same way getFieldValue() already does (a Vue computed/watch calling
  // this closure tracks Form's reactive prop directly, no boundary-crossing needed).
  getDisabled?: () => boolean
}): FormEngine {
  const { getModel, getFormRules, getDisabled = () => false } = opts
  const registry = new Map<string, () => FormRule[]>()
  const listeners = new Set<(errors: Record<string, string>) => void>()
  let errors: Record<string, string> = {}
  const initial = snapshot(getModel())

  const notify = () => {
    const snap = { ...errors }
    listeners.forEach((l) => l(snap))
  }

  // table-level rules (Form.rules[prop]) + field-level rules (from registry),
  // field-level appended AFTER table-level (both run; field does not override).
  const combinedRules = (prop: string): FormRule[] => [
    ...normalizeRules(getFormRules()[prop]),
    ...(registry.get(prop)?.() ?? []),
  ]

  return {
    registerField(prop, getRules) {
      registry.set(prop, getRules)
    },
    unregisterField(prop) {
      registry.delete(prop)
      if (prop in errors) {
        delete errors[prop]
        notify()
      }
    },
    getFieldValue(prop) {
      return (getModel() ?? {})[prop]
    },
    getError(prop) {
      return errors[prop] ?? ''
    },
    getDisabled() {
      return getDisabled()
    },
    async validateField(prop, trigger) {
      const model = getModel() ?? {}
      const all = combinedRules(prop)
      const applicable = trigger ? all.filter((r) => ruleMatchesTrigger(r, trigger)) : all
      // Triggered validation with no matching rules is a no-op (keeps prior error).
      if (trigger && applicable.length === 0) return errors[prop] ?? ''
      const error = runRules(model[prop], applicable, model)
      errors[prop] = error
      notify()
      return error
    },
    async validate() {
      const model = getModel() ?? {}
      const next: Record<string, string> = {}
      for (const prop of registry.keys()) {
        next[prop] = runRules(model[prop], combinedRules(prop), model)
      }
      errors = next
      notify()
      return { valid: Object.values(next).every((e) => e === ''), errors: { ...next } }
    },
    resetFields() {
      const model = getModel() ?? {}
      for (const key of Object.keys(initial)) {
        // mutate in place so a reactive model (Vue) picks the restore up
        model[key] = JSON.parse(JSON.stringify(initial[key]))
      }
      errors = {}
      notify()
    },
    clearValidate(props) {
      if (props == null) {
        errors = {}
      } else {
        const list = Array.isArray(props) ? props : [props]
        for (const p of list) delete errors[p]
      }
      notify()
    },
    subscribe(listener) {
      listeners.add(listener)
      return () => listeners.delete(listener)
    },
  }
}
