// Validación de integridad server-side del submit (Etapa 4G).
// REUSA lib/form-engine.ts (resolveVisibility, resolveScreens, expandInstances) —
// no reimplementa lógica de flujo. Corre en el servidor (Server Action), nunca
// en el cliente: "la validación de cliente es UX, NO seguridad" (BRIEF decisión 15).
//
// Lo que esta función NO hace: persistir nada. Solo decide si el payload recibido
// es coherente con el esquema + el propio flujo declarado por el payload, y si es
// así, devuelve las filas listas para insertar (mapeadas a question_id).

import type { AnswerValue, FormAnswers, FormSchema, Question } from './form-engine.types'
// Extensión .ts explícita (no solo en .mts): a diferencia de form-engine.ts —
// que solo importa *tipos* de form-engine.types (se borran en runtime, Node
// nunca necesita resolverlos) — este archivo importa FUNCIONES reales y
// corre tanto bajo Next.js (Server Action) como bajo Node puro (tests). Node
// exige extensión explícita para resolver imports de valor; tsconfig.json
// habilita allowImportingTsExtensions para permitir esto sin romper tsc.
import { answerKey, expandInstances, isAnswerEmpty, resolveScreens, resolveVisibility } from './form-engine.ts'

export interface SubmitAnswerEntry {
  code: string
  /** 'default' para preguntas normales; 'familiar_N' para preguntas de instancia. */
  instanceId: string
  value: AnswerValue
}

export interface ValidatedRow {
  questionId: string
  instanceId: string
  value: AnswerValue
}

export type ValidationResult =
  | { ok: true; rows: ValidatedRow[] }
  | { ok: false; errors: string[] }

function validateValueShape(question: Question, value: AnswerValue): string | null {
  switch (question.type) {
    case 'yes_no':
      return value === 'true' || value === 'false' ? null : 'invalid_yes_no_value'

    case 'single_choice': {
      if (typeof value !== 'string') return 'expected_string_value'
      if (question.options && !question.options.some((o) => o.value === value)) {
        return 'value_not_in_options'
      }
      return null
    }

    case 'multiple_choice': {
      if (!Array.isArray(value)) return 'expected_array_value'
      if (!value.every((v) => typeof v === 'string')) return 'expected_string_array'
      if (question.max_select !== null && value.length > question.max_select) {
        return 'exceeds_max_select'
      }
      if (question.options) {
        const valid = new Set(question.options.map((o) => o.value))
        if (!value.every((v) => valid.has(v as string))) return 'value_not_in_options'
      }
      return null
    }

    case 'text_short':
    case 'text_long':
      return typeof value === 'string' ? null : 'expected_string_value'

    // rating/nps/matrix/ranking/date/slider: no usados por el instrumento v3 hoy.
    // No se valida su forma específica — si en el futuro se usan, agregar el caso.
    default:
      return null
  }
}

/**
 * Corre las 7 validaciones del punto 4 de ETAPA_4G_PROMPT.md contra el
 * esquema cargado de la DB. Si CUALQUIERA falla, devuelve ok:false con la
 * lista completa de errores encontrados (no solo el primero) — el caller
 * rechaza el submit entero, sin insertar nada.
 */
export function validateSubmission(
  schema: FormSchema,
  entries: SubmitAnswerEntry[],
  consentOperativo: boolean
): ValidationResult {
  const errors: string[] = []
  const byCode = new Map(schema.questions.map((q) => [q.code, q] as const))

  // Reconstruye el mismo FormAnswers que tuvo el wizard — las funciones del
  // motor lo necesitan para evaluar reglas/instancias/visibilidad.
  const answers: FormAnswers = {}
  for (const e of entries) answers[answerKey(e.code, e.instanceId)] = e.value

  // 6. Consentimiento operativo requerido presente
  if (!consentOperativo) errors.push('consent_operativo_required')

  const nonEmpty = entries.filter((e) => !isAnswerEmpty(e.value))

  // 2. Preguntas legítimas · 3. Valores válidos · 5. Instancias legítimas
  for (const e of nonEmpty) {
    const q = byCode.get(e.code)
    if (!q) { errors.push(`unknown_question:${e.code}`); continue }
    if (q.type === 'section') { errors.push(`section_is_not_answerable:${e.code}`); continue }

    if (q.instance_of) {
      const generator = byCode.get(q.instance_of)
      const instances = generator ? expandInstances(generator, answers) : []
      if (!instances.some((i) => i.instanceId === e.instanceId)) {
        errors.push(`invalid_instance:${e.code}:${e.instanceId}`)
        continue
      }
    } else if (e.instanceId !== 'default') {
      errors.push(`unexpected_instance_id:${e.code}:${e.instanceId}`)
      continue
    }

    const shapeError = validateValueShape(q, e.value)
    if (shapeError) errors.push(`${shapeError}:${e.code}`)
  }
  if (errors.length > 0) return { ok: false, errors }

  // 4. Visibilidad coherente — cada respuesta corresponde a algo que el
  // flujo (con las respuestas recibidas) debía mostrar. Usa resolveVisibility
  // del motor directamente — ninguna lógica de show/hide se repite acá.
  for (const e of nonEmpty) {
    const q = byCode.get(e.code)!
    if (!resolveVisibility(q, answers)) {
      errors.push(`question_should_be_hidden:${e.code}:${e.instanceId}`)
    }
  }
  if (errors.length > 0) return { ok: false, errors }

  // 7. Requeridas presentes — recorre las pantallas que el motor resuelve
  // como visibles dadas estas respuestas, exige valor donde required=true.
  const screens = resolveScreens(schema, answers)
  for (const s of screens) {
    if (s.kind !== 'question' || !s.question?.required) continue
    const v = answers[answerKey(s.question.code, s.instanceId ?? undefined)]
    if (isAnswerEmpty(v)) {
      errors.push(`required_missing:${s.question.code}:${s.instanceId ?? 'default'}`)
    }
  }
  if (errors.length > 0) return { ok: false, errors }

  const rows: ValidatedRow[] = nonEmpty.map((e) => ({
    questionId: byCode.get(e.code)!.id,
    instanceId: e.instanceId,
    value: e.value,
  }))

  return { ok: true, rows }
}
