// Motor de resolución de flujo v3 (ADR-005) — módulo puro, sin React ni DOM.
// Reemplaza conceptualmente a lib/conditional.ts (v2); el componente (4D) decide
// cuándo migrar. Entrada→salida determinista: dado un FormSchema y un FormAnswers,
// siempre resuelve la misma lista de pantallas.

import type {
  AnswerValue,
  ConditionalRule,
  FormAnswers,
  FormSchema,
  Question,
  Screen,
  WizardState,
} from './form-engine.types'

const DEFAULT_INSTANCE = 'default'

export function answerKey(code: string, instanceId: string = DEFAULT_INSTANCE): string {
  return `${code}:${instanceId}`
}

function getAnswer(answers: FormAnswers, code: string, instanceId = DEFAULT_INSTANCE): AnswerValue {
  const v = answers[answerKey(code, instanceId)]
  return v === undefined ? null : v
}

/**
 * Evalúa una regla individual. `if_question` siempre se resuelve contra
 * instance_id='default': en el vocabulario v3 los disparadores (3.1, 4.1)
 * nunca son preguntas de instancia. Si en el futuro una regla necesitara
 * compararse contra la respuesta de la PROPIA instancia, este es el punto
 * a extender (hoy no hace falta — no hay caso de uso en el seed v3).
 */
export function evaluateRule(rule: ConditionalRule, answers: FormAnswers): boolean {
  switch (rule.op) {
    case 'eq':
      return getAnswer(answers, rule.if_question) === rule.value
    case 'eq_any':
      return rule.if_question.some((code) => getAnswer(answers, code) === rule.value)
    case 'not_in': {
      const v = getAnswer(answers, rule.if_question)
      if (typeof v !== 'string') return false
      return !rule.value.includes(v)
    }
    case 'contains': {
      const v = getAnswer(answers, rule.if_question)
      return Array.isArray(v) && v.includes(rule.value)
    }
    case 'eq_sole': {
      const v = getAnswer(answers, rule.if_question)
      return Array.isArray(v) && v.length === 1 && v[0] === rule.value
    }
    default:
      return false
  }
}

/**
 * Semántica de combinación show/hide (ver reporte en la conversación):
 * 1. hide es veto absoluto — si CUALQUIER regla hide coincide, se oculta.
 * 2. Si ninguna hide coincide y hay reglas show, se muestra solo si
 *    CUALQUIERA show coincide (OR); si hay show pero ninguna coincide, se oculta.
 * 3. Si no hay ninguna regla show (solo hide, o array vacío/null), se
 *    muestra por defecto.
 */
export function resolveVisibility(question: Question, answers: FormAnswers): boolean {
  const rules = question.conditional_rules
  if (!rules || rules.length === 0) return true

  const hideRules = rules.filter((r) => r.action === 'hide')
  const showRules = rules.filter((r) => r.action === 'show')

  if (hideRules.some((r) => evaluateRule(r, answers))) return false
  if (showRules.length > 0) return showRules.some((r) => evaluateRule(r, answers))
  return true
}

export interface InstanceRef {
  instanceId: string
  optionValue: string
}

/**
 * Deriva las instancias activas desde la respuesta del generador.
 * El orden e id de cada instancia se basan en la POSICIÓN FIJA de la opción
 * dentro de `generator.options` (definida por el esquema), nunca en el orden
 * de selección del usuario ni en un índice del array de respuesta — así una
 * instancia existente conserva su id estable aunque se agregue o quite otro
 * vínculo (lección del mockup: el índice se desincroniza con las instancias).
 */
export function expandInstances(generator: Question, answers: FormAnswers): InstanceRef[] {
  const selected = getAnswer(answers, generator.code)
  if (!Array.isArray(selected) || !generator.options) return []
  const refs: InstanceRef[] = []
  generator.options.forEach((opt, idx) => {
    if (selected.includes(opt.value)) {
      refs.push({ instanceId: `familiar_${idx + 1}`, optionValue: opt.value })
    }
  })
  return refs
}

export function buildFormSchema(rows: Question[]): FormSchema {
  return { questions: [...rows].sort((a, b) => a.position - b.position) }
}

/**
 * Resuelve la lista ordenada de pantallas visibles dado el esquema y el
 * estado de respuestas actual. Pura: mismo input → mismo output siempre.
 * Las preguntas instance_of se expanden agrupadas POR INSTANCIA (no por
 * pregunta), para reproducir el orden familiar1: 4.2→4.3, familiar2: 4.2→4.3...
 */
export function resolveScreens(schema: FormSchema, answers: FormAnswers): Screen[] {
  const screens: Screen[] = []
  const consumedGroups = new Set<string>()
  const byCode = new Map(schema.questions.map((q) => [q.code, q] as const))

  for (const q of schema.questions) {
    if (q.instance_of) {
      if (consumedGroups.has(q.instance_of)) continue
      consumedGroups.add(q.instance_of)

      const generator = byCode.get(q.instance_of)
      if (!generator) continue

      const instances = expandInstances(generator, answers)
      const groupRows = schema.questions
        .filter((r) => r.instance_of === q.instance_of)
        .sort((a, b) => a.position - b.position)

      for (const instance of instances) {
        for (const row of groupRows) {
          if (resolveVisibility(row, answers)) {
            screens.push({
              id: `question:${row.code}:${instance.instanceId}`,
              kind: 'question',
              question: row,
              instanceId: instance.instanceId,
            })
          }
        }
      }
      continue
    }

    if (q.type === 'section') {
      if (resolveVisibility(q, answers)) {
        screens.push({ id: `section:${q.code}`, kind: 'section', question: q, instanceId: null })
      }
      continue
    }

    if (resolveVisibility(q, answers)) {
      screens.push({ id: `question:${q.code}`, kind: 'question', question: q, instanceId: null })
    }
  }

  screens.push({ id: 'end', kind: 'end', question: null, instanceId: null })
  return screens
}

export function initialState(schema: FormSchema): WizardState {
  const screens = resolveScreens(schema, {})
  return { answers: {}, currentScreenId: screens[0]?.id ?? 'end' }
}

export function setAnswer(
  state: WizardState,
  code: string,
  value: AnswerValue,
  instanceId: string = DEFAULT_INSTANCE
): WizardState {
  return {
    ...state,
    answers: { ...state.answers, [answerKey(code, instanceId)]: value },
  }
}

/** Exportada porque también la usa el validador de submit server-side (4G). */
export function isAnswerEmpty(v: AnswerValue | undefined): boolean {
  return v === null || v === undefined || v === '' || (Array.isArray(v) && v.length === 0)
}

/**
 * Aplica una respuesta y limpia las respuestas de pantallas que dejaron de
 * ser visibles como CONSECUENCIA de ese cambio (ej.: sacar un vínculo de 4.1
 * borra las respuestas de 4.2/4.3 de esa instancia puntual; las de las demás
 * instancias quedan intactas porque conservan su propio answerKey). Esto es
 * consecuencia de flujo (ADR-005), no presentación — por eso vive en el
 * motor y no en el componente.
 */
export function applyAnswer(
  state: WizardState,
  schema: FormSchema,
  code: string,
  value: AnswerValue,
  instanceId: string = DEFAULT_INSTANCE
): WizardState {
  const updated = setAnswer(state, code, value, instanceId)
  const before = resolveScreens(schema, state.answers)
  const after = resolveScreens(schema, updated.answers)
  const afterIds = new Set(after.map((s) => s.id))
  const disappeared = before.filter((s) => !afterIds.has(s.id) && s.question)

  if (disappeared.length === 0) return updated

  const clearedAnswers = { ...updated.answers }
  for (const s of disappeared) {
    delete clearedAnswers[answerKey(s.question!.code, s.instanceId ?? undefined)]
  }
  return { ...updated, answers: clearedAnswers }
}

/**
 * true si la pantalla es una pregunta `required` y todavía no tiene
 * respuesta — incluye preguntas de instancia (cada instancia se evalúa por
 * su propio answerKey, independiente de las demás). El seed v3 actual no
 * marca ninguna pregunta de instancia como required, pero el camino queda
 * cubierto para cuando exista una.
 */
export function isRequiredAndUnanswered(screen: Screen, answers: FormAnswers): boolean {
  if (!screen.question?.required) return false
  return isAnswerEmpty(answers[answerKey(screen.question.code, screen.instanceId ?? undefined)])
}

/**
 * Avanza una pantalla. Recalcula la lista de pantallas visibles desde las
 * respuestas actuales y busca la pantalla actual por ID (nunca por índice
 * guardado de antes) — así, si el cambio de respuesta ocultó/agregó
 * pantallas, next() sigue aterrizando en la pantalla correcta.
 */
export function next(state: WizardState, schema: FormSchema): WizardState {
  const screens = resolveScreens(schema, state.answers)
  const idx = screens.findIndex((s) => s.id === state.currentScreenId)
  const nextIdx = Math.max(Math.min(idx + 1, screens.length - 1), 0)
  return { ...state, currentScreenId: screens[nextIdx].id }
}

export function back(state: WizardState, schema: FormSchema): WizardState {
  const screens = resolveScreens(schema, state.answers)
  const idx = screens.findIndex((s) => s.id === state.currentScreenId)
  const prevIdx = idx <= 0 ? 0 : idx - 1
  return { ...state, currentScreenId: screens[prevIdx]?.id ?? state.currentScreenId }
}
