/* MACU RACE 2026 — Main App */

const { useState, useEffect, useRef, useMemo } = React;

// ===== Declaración de responsabilidad — placeholder con varias secciones =====
function DeclaracionModal({ onAccept, onClose }) {
  const bodyRef = useRef(null);
  const closeBtnRef = useRef(null);
  const acceptBtnRef = useRef(null);
  const previousFocusRef = useRef(null);
  const [scrolledEnd, setScrolledEnd] = useState(false);

  const checkScrollEnd = () => {
    const el = bodyRef.current;
    if (!el) return;
    // Si todavía no llegó al final, no marcamos. Margen de 4px para tolerar redondeos del navegador.
    if (el.scrollHeight - el.scrollTop - el.clientHeight < 4) setScrolledEnd(true);
  };

  // Foco al abrir, restaurar al cerrar, Esc para cerrar.
  useEffect(() => {
    previousFocusRef.current = document.activeElement;
    if (closeBtnRef.current) closeBtnRef.current.focus();
    // Si el contenido no requiere scroll (cabe completo), habilitar aceptar de inmediato.
    const el = bodyRef.current;
    if (el && el.scrollHeight <= el.clientHeight + 4) setScrolledEnd(true);

    const onKey = (ev) => { if (ev.key === 'Escape') onClose(); };
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('keydown', onKey);
      if (previousFocusRef.current && previousFocusRef.current.focus) {
        previousFocusRef.current.focus();
      }
    };
  }, [onClose]);

  // Cuando se habilita el botón aceptar, lo enfocamos para que sea obvio.
  useEffect(() => {
    if (scrolledEnd && acceptBtnRef.current) acceptBtnRef.current.focus();
  }, [scrolledEnd]);

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" role="dialog" aria-modal="true" aria-labelledby="decl-title" onClick={e => e.stopPropagation()}>
        <div className="modal__head">
          <h3 id="decl-title">Declaración de responsabilidad y exoneración</h3>
          <button ref={closeBtnRef} className="modal__close" aria-label="Cerrar declaración" onClick={onClose}>×</button>
        </div>
        <div className="modal__body" ref={bodyRef} onScroll={checkScrollEnd} tabIndex={0}>
          <p><strong>Macu Race 2026 — Gimnasio Bilingüe Campestre Marie Curie</strong></p>
          <p>Al firmar esta declaración manifiesto que participo voluntariamente en la carrera Macu Race 2026 organizada por el Gimnasio Bilingüe Campestre Marie Curie (GBCMC), aceptando las condiciones reglamentarias del evento y reconociendo los riesgos propios de una actividad deportiva al aire libre.</p>

          <h4>1. Estado de salud</h4>
          <p>Declaro que me encuentro en condiciones físicas y de salud adecuadas para participar en la modalidad seleccionada (5K, 10K, 15K, Pet Race, Élite o Macu Race Kids). He consultado con un médico en caso de tener antecedentes cardiovasculares, respiratorios u osteomusculares.</p>

          <h4>2. Riesgos asumidos</h4>
          <p>Reconozco que la práctica de la carrera implica riesgos previsibles tales como caídas, esguinces, deshidratación, agotamiento, contacto con otros participantes y condiciones climáticas variables. Asumo plena responsabilidad sobre cualquier consecuencia derivada de mi participación.</p>

          <h4>3. Exoneración</h4>
          <p>Exonero al GBCMC, a sus organizadores, patrocinadores, voluntarios y demás entidades vinculadas, de toda responsabilidad civil o penal derivada de lesiones, daños materiales o cualquier incidente que pudiera presentarse antes, durante o después de la carrera.</p>

          <h4>4. Uso de imagen</h4>
          <p>Autorizo al GBCMC para captar, editar y publicar mi imagen en fotografías y videos relacionados con el evento, con fines informativos, promocionales y de archivo institucional, sin contraprestación económica.</p>

          <h4>5. Datos personales</h4>
          <p>Autorizo al GBCMC para el tratamiento de mis datos personales conforme a la Ley 1581 de 2012 y su política de protección de datos. Los datos se usarán exclusivamente para la gestión del evento, comunicaciones relacionadas y trámites legales.</p>

          <h4>6. Reglamento</h4>
          <p>Me comprometo a respetar el reglamento del evento, las indicaciones del personal logístico y de seguridad, así como a portar visible el dorsal y el chip de cronometraje durante toda la prueba.</p>

          <h4>7. Menores de edad</h4>
          <p>En caso de inscribir a un menor de edad (Macu Race Kids), declaro ser su padre, madre o acudiente legal y asumir la responsabilidad por su participación, autorizando la prestación de primeros auxilios si fuera necesario.</p>

          <h4>8. Mascotas (5K Pet Race)</h4>
          <p>Si participo en la modalidad Pet Race, declaro que mi mascota cuenta con esquema completo de vacunación, traílla adecuada y se encuentra en condiciones de salud aptas para una caminata moderada.</p>

          <p style={{marginTop:20, color:'var(--azul-macu)', fontWeight:600}}>Al hacer clic en "Acepto y continúo" confirmo que he leído íntegramente esta declaración y la acepto en todos sus términos.</p>
        </div>
        <div className="modal__foot">
          <span className={"modal__scroll-hint " + (scrolledEnd ? 'is-done':'')}>
            {scrolledEnd ? '✓ Listo, puedes aceptar' : '↓ Lee hasta el final para habilitar el botón'}
          </span>
          <button ref={acceptBtnRef} className="btn btn-primary" disabled={!scrolledEnd} onClick={onAccept}>
            Acepto y continúo
          </button>
        </div>
      </div>
    </div>
  );
}

// Algo más estricta que `.+@.+\..+` pero sin caer en RFC 5322 completo.
// Rechaza espacios, dobles puntos y exige TLD de al menos 2 caracteres alfabéticos.
const EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*\.[A-Za-z]{2,}$/;

// Reglas de coherencia entre tipo de documento y edad.
//   CC / CE  → mayor de edad (≥ 18)
//   TI / RC  → menor de edad (6 a 17)
//   PA / Otro → sin restricción
const DOC_ADULTO = ['CC', 'CE'];
const DOC_MENOR  = ['TI', 'RC'];

// ===== Validation per step =====
function validateStep(step, data) {
  const e = {};
  if (step === 1) {
    if (!data.tipoDoc) e.tipoDoc = 'Selecciona el tipo de documento.';
    if (!data.numDoc || data.numDoc.length < 5) e.numDoc = 'Mínimo 5 dígitos.';
    if (!data.acepto) e.acepto = 'Debes aceptar la declaración de responsabilidad.';
  }
  if (step === 2) {
    if (!data.nombre || data.nombre.trim().length < 3) e.nombre = 'Ingresa tu nombre completo.';
    if (!data.genero) e.genero = 'Selecciona el género.';
    if (!data.email || !EMAIL_RE.test(data.email.trim())) e.email = 'Ingresa un correo válido.';

    const telDigits = (data.tel || '').replace(/\D/g, '');
    if (!telDigits) {
      e.tel = 'Ingresa el número de contacto.';
    } else if (data.tipoTel === 'Celular' && telDigits.length !== 10) {
      e.tel = 'El celular debe tener 10 dígitos.';
    } else if (data.tipoTel === 'Fijo' && (telDigits.length < 7 || telDigits.length > 10)) {
      e.tel = 'El teléfono fijo debe tener entre 7 y 10 dígitos.';
    }

    const age = Number(data.edad);
    if (!data.edad || age === 0) e.edad = 'Indica la edad.';
    else if (age < 6) e.edad = 'La edad mínima es 6 años (Macu Race Kids).';
    else if (age > 99) e.edad = 'Edad inválida.';
    else if (DOC_ADULTO.includes(data.tipoDoc) && age < 18) {
      e.edad = 'Con CC o CE el participante debe ser mayor de edad (≥ 18). Cambia el tipo de documento en el paso 1.';
    } else if (DOC_MENOR.includes(data.tipoDoc) && age >= 18) {
      e.edad = 'Con TI o RC el participante debe ser menor de edad (6 a 17). Cambia el tipo de documento en el paso 1.';
    }
  }
  if (step === 3) {
    if (!data.eps) e.eps = 'Selecciona tu EPS.';
    else if (data.eps === 'Otra' && (!data.epsOtra || data.epsOtra.trim().length < 2)) {
      e.epsOtra = 'Escribe el nombre de tu EPS.';
    }
    if (!data.sangre) e.sangre = 'Selecciona grupo sanguíneo.';
    if (!data.emergName || data.emergName.length < 3) e.emergName = 'Nombre del contacto.';
    const emergDigits = (data.emergTel || '').replace(/\D/g, '');
    if (!emergDigits) e.emergTel = 'Ingresa el teléfono de emergencia.';
    else if (emergDigits.length !== 10) e.emergTel = 'El teléfono debe tener 10 dígitos.';
  }
  if (step === 4) {
    if (!data.distId) e.distId = 'Elige una modalidad.';
    if (!data.talla) e.talla = 'Selecciona tu talla de camiseta.';
  }
  if (step === 5) {
    if (data.hasVinculo === null) e.hasVinculo = 'Indica si tienes vínculo.';
    if (data.hasVinculo === true) {
      if (!data.tipoVinculo) e.tipoVinculo = 'Selecciona el tipo de vínculo.';
      if (!data.soporte) e.soporte = 'Adjunta el soporte de tu vínculo.';

      if (VINC_PIDE_CODIGO_CURSO.includes(data.tipoVinculo)) {
        if (!data.codigoEstudiante || data.codigoEstudiante.trim().length < 3) {
          e.codigoEstudiante = 'Ingresa el código de estudiante (mín. 3 caracteres).';
        }
        if (!data.curso || data.curso.trim().length < 1) {
          e.curso = 'Indica el curso.';
        }
      }
      if (VINC_PIDE_ANO.includes(data.tipoVinculo)) {
        const ano = Number(data.anoGraduacion);
        if (!data.anoGraduacion || data.anoGraduacion.length !== 4 || !Number.isInteger(ano)) {
          e.anoGraduacion = 'Ingresa el año de graduación (4 dígitos).';
        } else if (ano < 1950 || ano > 2026) {
          e.anoGraduacion = 'Año fuera de rango (1950 – 2026).';
        }
      }
      if (VINC_PIDE_CARGO.includes(data.tipoVinculo)) {
        if (!data.cargoInstitucion || data.cargoInstitucion.trim().length < 3) {
          e.cargoInstitucion = 'Especifica el cargo en la institución.';
        }
      }
    }
  }
  return e;
}

function ProgressBar({ step, onJump }) {
  return (
    <ol className="progress" role="list" aria-label="Progreso de la inscripción">
      {window.STEPS.map(s => {
        const status = s.id < step ? 'done' : s.id === step ? 'active' : 'todo';
        const cls = status === 'done' ? 'is-done' : status === 'active' ? 'is-active' : '';
        const clickable = s.id < step;
        const ariaLabel = `Paso ${s.id} de ${window.STEPS.length}: ${s.long}${status === 'active' ? ' (actual)' : status === 'done' ? ' (completado)' : ''}`;
        const inner = (
          <>
            <div className="progress__circle" aria-hidden="true">
              {s.id < step ? <Icon.Check s={14}/> : s.id}
            </div>
            <div className="progress__label" aria-hidden="true">{s.short}</div>
          </>
        );
        return (
          <li key={s.id} className={"progress__step " + cls} aria-current={status === 'active' ? 'step' : undefined}>
            {clickable ? (
              <button type="button" className="progress__btn" aria-label={ariaLabel} onClick={() => onJump(s.id)}>
                {inner}
              </button>
            ) : (
              <div className="progress__btn progress__btn--static" aria-label={ariaLabel}>
                {inner}
              </div>
            )}
          </li>
        );
      })}
    </ol>
  );
}

function Summary({ data, distObj, precio, step }) {
  const tipoVinculoLabel = (window.TIPOS_VINCULO.find(t => t.id === data.tipoVinculo) || {}).label;
  return (
    <aside className="summary">
      <div className="summary__head">🏁 Tu inscripción</div>
      <div className="summary__row"><span>Participante</span><span>{data.nombre || '—'}</span></div>
      <div className="summary__row"><span>Edad</span><span>{data.edad ? data.edad + ' años' : '—'}</span></div>
      <div className="summary__row"><span>Modalidad</span><span>{distObj ? distObj.name : '—'}</span></div>
      <div className="summary__row"><span>Categoría</span><span>{distObj ? distObj.cat : '—'}</span></div>
      <div className="summary__row"><span>Talla</span><span>{data.talla || '—'}</span></div>
      <div className="summary__row"><span>Vínculo</span><span>{data.hasVinculo === null ? '—' : data.hasVinculo ? (tipoVinculoLabel || 'Sí') : 'Público'}</span></div>

      <div className="summary__price">
        <div className="summary__price-label">Valor</div>
        <div className="summary__price-val">
          {precio.price != null ? window.fmtCOP(precio.price) : '—'}
          {precio.price != null && <small>COP</small>}
        </div>
        <div style={{fontSize:12, opacity:.7, marginTop:4}}>{precio.label}</div>
      </div>

      <div className="summary__beneficios">
        <h4>Tu inscripción incluye</h4>
        <ul>
          <li><Icon.Check s={14}/> Camiseta oficial Macu Race</li>
          <li><Icon.Check s={14}/> Chip de cronometraje</li>
          <li><Icon.Check s={14}/> Hidratación en ruta</li>
          <li><Icon.Check s={14}/> Refrigerio post-meta</li>
          <li><Icon.Check s={14}/> Medalla de participación</li>
          <li><Icon.Check s={14}/> Seguro contra accidentes</li>
        </ul>
      </div>
    </aside>
  );
}

function MobileSummary({ data, distObj, precio }) {
  const [open, setOpen] = useState(false);
  const tipoVinculoLabel = (window.TIPOS_VINCULO.find(t => t.id === data.tipoVinculo) || {}).label;
  return (
    <div className="mob-summary">
      <div className="mob-summary__head">
        <div>
          <div className="mob-summary__lbl">Valor a pagar</div>
          <div className="mob-summary__price">
            {precio.price != null ? window.fmtCOP(precio.price) : '—'}
          </div>
        </div>
        <button
          type="button"
          className="mob-summary__toggle"
          aria-expanded={open}
          aria-controls="mob-summary-details"
          onClick={() => setOpen(o => !o)}>
          {open ? 'Ocultar' : 'Ver detalle'}
        </button>
      </div>
      <div className="mob-summary__sublabel">{precio.label}</div>
      {open && (
        <div id="mob-summary-details" className="mob-summary__details">
          <div className="mob-summary__row"><span>Participante</span><span>{data.nombre || '—'}</span></div>
          <div className="mob-summary__row"><span>Edad</span><span>{data.edad ? data.edad + ' años' : '—'}</span></div>
          <div className="mob-summary__row"><span>Modalidad</span><span>{distObj ? distObj.name : '—'}</span></div>
          <div className="mob-summary__row"><span>Categoría</span><span>{distObj ? distObj.cat : '—'}</span></div>
          <div className="mob-summary__row"><span>Talla</span><span>{data.talla || '—'}</span></div>
          <div className="mob-summary__row"><span>Vínculo</span><span>{data.hasVinculo === null ? '—' : data.hasVinculo ? (tipoVinculoLabel || 'Sí') : 'Público'}</span></div>
        </div>
      )}
    </div>
  );
}

// ===== Pantallas post-formulario =====

// Pantalla de carga inicial mientras se verifica localStorage / fetch de estado.
function LoadingScreen({ message }) {
  return (
    <div className="success-screen">
      <div className="spinner-lg" aria-hidden="true"/>
      <p style={{marginTop:24}}>{message || 'Cargando…'}</p>
    </div>
  );
}

// El usuario ya inició una inscripción pero no la pagó. Le ofrecemos retomar o empezar de cero.
function ResumeScreen({ remote, onContinue, onDiscard }) {
  const minutos = remote && remote.createdAt
    ? Math.round((Date.now() - new Date(remote.createdAt).getTime()) / 60000)
    : 0;
  return (
    <div className="success-screen">
      <div className="success-screen__icon" style={{background:'#f59e0b'}}>⏱</div>
      <h2>Tienes una inscripción sin pagar</h2>
      <p>
        {remote && remote.nombre ? <>Hola <strong>{remote.nombre.split(' ')[0]}</strong>, </> : null}
        iniciaste tu inscripción hace {minutos < 1 ? 'unos segundos' : `${minutos} min`} pero el pago quedó pendiente.
      </p>
      <p style={{marginTop:14, fontSize:20, fontWeight:700}}>
        Total: {window.fmtCOP(remote && remote.precio)} COP
      </p>
      <div style={{display:'flex', flexDirection:'column', gap:10, marginTop:24, maxWidth:300, marginLeft:'auto', marginRight:'auto'}}>
        <button className="btn btn-primary" onClick={onContinue}>Continuar el pago</button>
        <button className="btn btn-ghost" onClick={onDiscard}>Empezar inscripción nueva</button>
      </div>
    </div>
  );
}

// Tras cerrar el widget de Wompi, esperamos confirmación del webhook (puede tardar segundos).
function ConfirmingScreen() {
  return (
    <div className="success-screen">
      <div className="spinner-lg" aria-hidden="true"/>
      <h2 style={{marginTop:20}}>Confirmando tu pago…</h2>
      <p>Estamos esperando la confirmación de Wompi. Esto suele tomar unos segundos.</p>
    </div>
  );
}

// Pago confirmado. Muestra el comprobante imprimible y opción para inscribir otra persona.
function PaidScreen({ remote, onNew }) {
  if (!remote) return <div className="success-screen"><p>Cargando…</p></div>;

  // Categoría se deriva del distId + edad usando la misma tabla del frontend.
  const opts = window.getDistancesForAge(remote.edad) || [];
  const distObj = opts.find(o => o.id === remote.distId);
  const modalidad = distObj?.label || distObj?.name || (remote.distId || '').toUpperCase();
  const categoria = distObj?.cat || '—';

  // Fecha legible en español
  const paidDate = remote.paidAt ? new Date(remote.paidAt) : null;
  const fechaLegible = paidDate
    ? paidDate.toLocaleString('es-CO', { dateStyle: 'long', timeStyle: 'short' })
    : '—';

  const docDisplay = remote.tipoDoc && remote.numDoc
    ? `${remote.tipoDoc} ${remote.numDoc}`
    : '—';

  const imprimir = () => window.print();

  return (
    <div className="recibo">
      {/* Header: solo visible en pantalla. En print se reemplaza por el header de impresión. */}
      <div className="recibo__icon no-print"><Icon.Check s={42}/></div>
      <h2 className="recibo__titulo no-print">¡Inscripción confirmada!</h2>
      <p className="recibo__intro no-print">
        Te enviaremos los detalles a <strong>{remote.email}</strong>.
      </p>

      {/* Header solo en impresión */}
      <div className="recibo__print-header print-only">
        <div className="recibo__brand">Gimnasio Bilingüe Campestre Marie Curie</div>
        <h1>MACU RACE 2026</h1>
        <div className="recibo__doc-type">Comprobante de inscripción</div>
      </div>

      {/* Dorsal — destacado en pantalla, prominente en impresión */}
      <div className="recibo__dorsal">
        <small>DORSAL</small>
        <strong>#{remote.bib}</strong>
      </div>

      {/* Bloques de datos */}
      <div className="recibo__grid">
        <div className="recibo__block">
          <h4>Participante</h4>
          <div className="recibo__row"><span>Nombre</span><span>{remote.nombre || '—'}</span></div>
          <div className="recibo__row"><span>Documento</span><span>{docDisplay}</span></div>
          <div className="recibo__row"><span>Edad</span><span>{remote.edad ? `${remote.edad} años` : '—'}</span></div>
          <div className="recibo__row"><span>Correo</span><span>{remote.email}</span></div>
        </div>

        <div className="recibo__block">
          <h4>Carrera</h4>
          <div className="recibo__row"><span>Modalidad</span><span>{modalidad}</span></div>
          <div className="recibo__row"><span>Categoría</span><span>{categoria}</span></div>
          <div className="recibo__row"><span>Talla</span><span>{remote.talla || '—'}</span></div>
        </div>

        <div className="recibo__block recibo__block--full">
          <h4>Pago</h4>
          <div className="recibo__row"><span>Valor pagado</span><span><strong>{window.fmtCOP(remote.precio)} COP</strong></span></div>
          <div className="recibo__row"><span>Tarifa</span><span>{remote.precioLabel}</span></div>
          <div className="recibo__row"><span>Fecha de pago</span><span>{fechaLegible}</span></div>
          <div className="recibo__row"><span>Ref. transacción</span><span className="recibo__mono">{remote.paymentRef || '—'}</span></div>
          <div className="recibo__row"><span>Ref. inscripción</span><span className="recibo__mono">{remote.inscripcionId}</span></div>
        </div>
      </div>

      <div className="recibo__legal print-only">
        Este documento es comprobante de pago de la inscripción al evento Macu Race 2026.
        Preséntalo el día del evento junto con tu documento de identidad.
      </div>

      <p className="recibo__cta no-print">Nos vemos en la línea de salida 🏃‍♀️🏃‍♂️</p>

      <div className="recibo__buttons no-print">
        <button type="button" className="btn btn-primary" onClick={imprimir}>
          🖨️ Imprimir / Guardar PDF
        </button>
        {onNew && (
          <button type="button" className="btn btn-ghost" onClick={onNew}>
            Inscribir a otra persona
          </button>
        )}
      </div>
    </div>
  );
}

// El pago falló o fue rechazado.
function FailedScreen({ remote, onRetry, onDiscard }) {
  return (
    <div className="success-screen">
      <div className="success-screen__icon" style={{background:'var(--rojo)', color:'white'}}>✕</div>
      <h2>El pago no se completó</h2>
      <p>
        {remote && remote.failedReason
          ? <>Wompi reportó: <strong>{remote.failedReason}</strong>.</>
          : 'No pudimos confirmar tu pago.'}
        {' '}Puedes reintentar o empezar una inscripción nueva.
      </p>
      <div style={{display:'flex', flexDirection:'column', gap:10, marginTop:24, maxWidth:300, marginLeft:'auto', marginRight:'auto'}}>
        <button className="btn btn-primary" onClick={onRetry}>Reintentar pago</button>
        <button className="btn btn-ghost" onClick={onDiscard}>Empezar de nuevo</button>
      </div>
    </div>
  );
}

// ===== Helpers para hablar con el backend =====

const API_BASE = () => (window.MACURACE_CONFIG && window.MACURACE_CONFIG.API_BASE) || '';
const LS_KEY = 'macurace:inscripcionId';

async function fetchEstado(inscripcionId) {
  const r = await fetch(`${API_BASE()}/inscripciones/${inscripcionId}/estado`);
  if (!r.ok) {
    if (r.status === 404) return null;
    throw new Error(`Estado HTTP ${r.status}`);
  }
  return await r.json();
}

async function fetchPagoConfig(inscripcionId) {
  const r = await fetch(`${API_BASE()}/inscripciones/${inscripcionId}/pagar`, { method: 'POST' });
  if (!r.ok) {
    const err = await r.json().catch(() => ({}));
    throw new Error(err.error || `Iniciar pago falló (${r.status})`);
  }
  return await r.json();
}

// Espera hasta que el webhook confirme el pago, máximo 30 segundos.
async function pollEstadoHastaResolver(inscripcionId, maxIntentos = 15, delayMs = 2000) {
  for (let i = 0; i < maxIntentos; i++) {
    const estado = await fetchEstado(inscripcionId);
    if (estado && estado.status !== 'pending') return estado;
    await new Promise(res => setTimeout(res, delayMs));
  }
  return await fetchEstado(inscripcionId);
}

// Abre el widget de Wompi. Resuelve cuando el usuario cierra el widget (haya pagado o no).
function abrirWidgetWompi(config) {
  const WidgetCheckout = window.WidgetCheckout;
  if (!WidgetCheckout) return Promise.reject(new Error('Widget de Wompi no cargado.'));

  // OJO: el WAF de CloudFront de Wompi rechaza `redirect-url` con localhost / 127.0.0.1
  // (devuelve 403 al iframe). Solo pasamos redirectUrl si apunta a un dominio real.
  // En dev usamos el callback de open() para detectar el cierre del widget.
  const isLocal = config.redirectUrl && /localhost|127\.0\.0\.1/i.test(config.redirectUrl);
  const widgetOptions = {
    currency: config.currency,
    amountInCents: config.amountInCents,
    reference: config.reference,
    publicKey: config.publicKey,
    signature: config.signature,
    customerData: config.customerData,
  };
  if (config.redirectUrl && !isLocal) widgetOptions.redirectUrl = config.redirectUrl;

  const checkout = new WidgetCheckout(widgetOptions);
  return new Promise((resolve) => {
    checkout.open((result) => resolve(result));
  });
}

// ===== Initial state =====
const INITIAL = {
  // step 1
  tipoDoc: '', numDoc: '', acepto: false,
  // step 2
  nombre: '', genero: '', email: '', tipoTel: 'Celular', tel: '', edad: '',
  // step 3
  eps: '', epsOtra: '', sangre: '', emergName: '', emergTel: '',
  // step 4
  distId: '', talla: '',
  // step 5
  hasVinculo: null, tipoVinculo: '', soporte: null,
  codigoEstudiante: '', curso: '', anoGraduacion: '', cargoInstitucion: '',
};

// Helpers para saber qué campo extra pide cada vínculo.
const VINC_PIDE_CODIGO_CURSO = ['estudiante', 'padre'];
const VINC_PIDE_ANO          = ['egresado'];
const VINC_PIDE_CARGO        = ['docente', 'hijo_colaborador'];

// ===== App =====
function App() {
  // screen: 'loading' | 'form' | 'resume' | 'paying' | 'confirming' | 'paid' | 'failed'
  const [screen, setScreen] = useState('loading');
  const [step, setStep] = useState(1);
  const [data, setData] = useState(INITIAL);
  const [errors, setErrors] = useState({});
  const [hasAttempted, setHasAttempted] = useState(false);
  const [showModal, setShowModal] = useState(false);
  const [remote, setRemote] = useState(null); // estado de la inscripción según el backend
  const [ageWipedSelections, setAgeWipedSelections] = useState(false);
  const [isPaying, setIsPaying] = useState(false);
  const [apiError, setApiError] = useState(null);

  // Idempotency-Key estable durante el ciclo de vida del componente.
  const idempotencyKeyRef = useRef(null);
  if (!idempotencyKeyRef.current && typeof crypto !== 'undefined' && crypto.randomUUID) {
    idempotencyKeyRef.current = crypto.randomUUID();
  }

  const set = (patch) => setData(d => ({ ...d, ...patch }));

  // ----- Resume-on-return: al montar, verificamos localStorage y consultamos el backend.
  useEffect(() => {
    const savedId = (typeof localStorage !== 'undefined') ? localStorage.getItem(LS_KEY) : null;
    if (!API_BASE()) {
      // Modo demo (sin backend configurado) → siempre empezamos en el formulario.
      setScreen('form');
      return;
    }
    if (!savedId) { setScreen('form'); return; }
    (async () => {
      try {
        const estado = await fetchEstado(savedId);
        if (!estado) {
          localStorage.removeItem(LS_KEY);
          setScreen('form');
          return;
        }
        setRemote(estado);
        if (estado.status === 'paid') setScreen('paid');
        else if (estado.status === 'pending') setScreen('resume');
        else { // failed | expired
          setScreen('failed');
        }
      } catch (err) {
        console.error('Error consultando inscripción guardada', err);
        setScreen('form'); // si el backend no responde, dejamos al usuario empezar de cero
      }
    })();
  }, []);

  // Cambiar la edad puede dejar la modalidad/talla fuera de rango: limpiar y avisar.
  useEffect(() => {
    if (!data.edad) return;
    const valid = window.getDistancesForAge(Number(data.edad)) || [];
    if (data.distId && !valid.find(v => v.id === data.distId)) {
      setData(d => ({ ...d, distId: '', talla: '' }));
      setAgeWipedSelections(true);
    }
  }, [data.edad]);

  // Apenas el usuario elige una nueva modalidad, ocultamos el aviso.
  useEffect(() => { if (data.distId) setAgeWipedSelections(false); }, [data.distId]);

  // Revalidación en vivo: una vez que el usuario intentó avanzar y falló,
  // los errores se recalculan en cada cambio del paso actual.
  useEffect(() => {
    if (!hasAttempted) return;
    setErrors(validateStep(step, data));
  }, [data, step, hasAttempted]);

  const age = Number(data.edad);
  const distObj = useMemo(() => {
    const opts = window.getDistancesForAge(age) || [];
    return opts.find(o => o.id === data.distId);
  }, [age, data.distId]);

  const precio = useMemo(() => window.calcularValor({
    age, distId: data.distId, hasVinculo: !!data.hasVinculo, tipoVinculo: data.tipoVinculo
  }), [age, data.distId, data.hasVinculo, data.tipoVinculo]);

  const goToStep = (n) => {
    setErrors({});
    setHasAttempted(false);
    setStep(n);
    window.scrollTo({ top: 0, behavior: 'smooth' });
  };

  const next = () => {
    const e = validateStep(step, data);
    setErrors(e);
    if (Object.keys(e).length === 0) {
      setHasAttempted(false);
      setStep(s => Math.min(6, s + 1));
      window.scrollTo({ top: 0, behavior: 'smooth' });
    } else {
      setHasAttempted(true);
    }
  };
  const back = () => goToStep(Math.max(1, step - 1));

  // ===== Flujo de pago =====
  //  1) POST /inscripciones (crea pending + presigned URL si hay soporte)
  //  2) PUT soporte a S3 (si aplica)
  //  3) POST /inscripciones/{id}/pagar (firma para Wompi)
  //  4) abrir widget Wompi
  //  5) tras cerrar widget → poll /estado hasta paid|failed|timeout
  const pay = async () => {
    if (isPaying) return;
    setIsPaying(true);
    setApiError(null);

    const api = API_BASE();

    // Modo demo (sin backend): comportamiento previo de simulación.
    if (!api) {
      setTimeout(() => {
        const fakeId = 'local-' + Math.random().toString(36).slice(2, 10);
        const fakeRemote = {
          inscripcionId: fakeId,
          status: 'paid',
          bib: String(1000 + Math.floor(Math.random() * 8999)),
          precio: precio.price,
          precioLabel: precio.label,
          distId: data.distId,
          talla: data.talla,
          nombre: data.nombre,
          email: data.email,
        };
        setRemote(fakeRemote);
        setScreen('paid');
        setIsPaying(false);
      }, 600);
      return;
    }

    try {
      // 1. POST /inscripciones
      const { soporteFile, ...payload } = data;
      const resp = await fetch(`${api}/inscripciones`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...(idempotencyKeyRef.current ? { 'Idempotency-Key': idempotencyKeyRef.current } : {}),
        },
        body: JSON.stringify(payload),
      });
      if (!resp.ok) {
        const err = await resp.json().catch(() => ({}));
        const msg = err.errors
          ? 'Hay errores en el formulario: ' + Object.values(err.errors).join(' ')
          : (err.error || `Error ${resp.status}`);
        throw new Error(msg);
      }
      const result = await resp.json();
      const id = result.inscripcionId;

      // Guardar en localStorage para resume-on-return.
      try { localStorage.setItem(LS_KEY, id); } catch {}

      // 2. Subir soporte si aplica
      if (result.presignedUpload && soporteFile) {
        const upload = await fetch(result.presignedUpload.url, {
          method: 'PUT',
          headers: { 'Content-Type': result.presignedUpload.contentType },
          body: soporteFile,
        });
        if (!upload.ok) throw new Error('No se pudo subir el archivo de soporte.');
      }

      // 3. Abrir widget de Wompi
      await abrirPagoWompi(id);
    } catch (err) {
      console.error('Error en flujo de pago', err);
      setApiError(err.message || 'Error inesperado. Intenta de nuevo.');
      setIsPaying(false);
    }
  };

  // Abre Wompi para una inscripcionId que ya existe (caso "Continuar pago" o tras crearInscripcion).
  const abrirPagoWompi = async (inscripcionId) => {
    setScreen('paying');
    try {
      const cfg = await fetchPagoConfig(inscripcionId);
      // Si el servidor responde que ya está paid, saltamos directo a éxito.
      if (cfg.status === 'paid') {
        const estado = await fetchEstado(inscripcionId);
        setRemote(estado);
        setScreen('paid');
        setIsPaying(false);
        return;
      }
      // Abrir widget Wompi (popup). El callback resuelve al cerrar.
      await abrirWidgetWompi(cfg);

      // Tras cerrar, sondeamos el backend hasta que el webhook actualice el estado.
      setScreen('confirming');
      const estadoFinal = await pollEstadoHastaResolver(inscripcionId);
      setRemote(estadoFinal);
      if (estadoFinal && estadoFinal.status === 'paid') setScreen('paid');
      else if (estadoFinal && estadoFinal.status === 'failed') setScreen('failed');
      else setScreen('resume'); // sigue pending: probablemente el usuario cerró sin pagar
      setIsPaying(false);
    } catch (err) {
      console.error('Error abriendo widget Wompi', err);
      setApiError(err.message || 'No se pudo abrir el widget de pago.');
      // Refrescamos el estado real para no quedarnos en pantalla equivocada.
      // Ej: si el servidor dice que ya está failed, vamos a la pantalla failed (no resume).
      try {
        const estadoActual = await fetchEstado(inscripcionId);
        if (estadoActual) {
          setRemote(estadoActual);
          if (estadoActual.status === 'paid') setScreen('paid');
          else if (estadoActual.status === 'failed') setScreen('failed');
          else setScreen('resume');
        } else {
          setScreen('form');
        }
      } catch {
        setScreen('resume');
      }
      setIsPaying(false);
    }
  };

  // Reset del estado para empezar una inscripción nueva (limpia localStorage y datos).
  const empezarNueva = () => {
    try { localStorage.removeItem(LS_KEY); } catch {}
    setRemote(null);
    setData(INITIAL);
    setStep(1);
    setErrors({});
    setHasAttempted(false);
    setApiError(null);
    setIsPaying(false);
    idempotencyKeyRef.current = (typeof crypto !== 'undefined' && crypto.randomUUID) ? crypto.randomUUID() : null;
    setScreen('form');
  };

  // ===== Render condicional según screen =====
  if (screen === 'loading') {
    return (
      <div className="page">
        <div className="card" style={{maxWidth: 640, margin:'0 auto'}}>
          <LoadingScreen message="Verificando tu inscripción…"/>
        </div>
      </div>
    );
  }

  if (screen === 'resume') {
    return (
      <div className="page" data-screen-label="Resume">
        <div className="card" style={{maxWidth: 640, margin:'0 auto'}}>
          {apiError && (
            <div className="notice notice--warn" role="alert" style={{borderLeftColor:'var(--rojo)', background:'#fff5f5', color:'var(--rojo)'}}>
              {apiError}
            </div>
          )}
          <ResumeScreen
            remote={remote}
            onContinue={() => abrirPagoWompi(remote.inscripcionId)}
            onDiscard={empezarNueva}/>
        </div>
      </div>
    );
  }

  if (screen === 'paying') {
    return (
      <div className="page">
        <div className="card" style={{maxWidth: 640, margin:'0 auto'}}>
          <LoadingScreen message="Preparando el pago… Si el widget no se abrió o muestra error, puedes volver atrás."/>
          <div style={{textAlign:'center', marginTop:20}}>
            <button className="btn btn-ghost" onClick={() => {
              setScreen(remote ? 'resume' : 'form');
              setIsPaying(false);
              setApiError('El proceso de pago se canceló. Verifica que los secretos de Wompi estén correctos y reintenta.');
            }}>
              Cancelar y volver
            </button>
          </div>
        </div>
      </div>
    );
  }

  if (screen === 'confirming') {
    return (
      <div className="page">
        <div className="card" style={{maxWidth: 640, margin:'0 auto'}}>
          <ConfirmingScreen/>
        </div>
      </div>
    );
  }

  if (screen === 'paid') {
    return (
      <div className="page" data-screen-label="Pagado">
        <div className="card" style={{maxWidth: 640, margin:'0 auto'}}>
          <PaidScreen remote={remote} onNew={empezarNueva}/>
        </div>
      </div>
    );
  }

  if (screen === 'failed') {
    return (
      <div className="page" data-screen-label="Fallido">
        <div className="card" style={{maxWidth: 640, margin:'0 auto'}}>
          <FailedScreen
            remote={remote}
            onRetry={() => remote && abrirPagoWompi(remote.inscripcionId)}
            onDiscard={empezarNueva}/>
        </div>
      </div>
    );
  }

  return (
    <div className="page" data-screen-label={`0${step} ${window.STEPS[step-1].long}`}>
      <ProgressBar step={step} onJump={goToStep}/>
      <MobileSummary data={data} distObj={distObj} precio={precio}/>
      <div className="layout">
        <div className="card">
          {step === 1 && <Step1Documento data={data} set={set} errors={errors} openModal={() => setShowModal(true)}/>}
          {step === 2 && <Step2Datos data={data} set={set} errors={errors} ageWipedSelections={ageWipedSelections}/>}
          {step === 3 && <Step3Salud data={data} set={set} errors={errors}/>}
          {step === 4 && <Step4Carrera data={data} set={set} errors={errors} ageWipedSelections={ageWipedSelections}/>}
          {step === 5 && <Step5Vinculo data={data} set={set} errors={errors}/>}
          {step === 6 && <Step6Resumen data={data} precio={precio} distObj={distObj} onPay={pay} onJump={goToStep} isPaying={isPaying} apiError={apiError}/>}

          {step < 6 && (
            <div className="btn-row">
              {step > 1
                ? <button className="btn btn-ghost" onClick={back}><Icon.Back s={16}/> Atrás</button>
                : <span/>}
              <button className="btn btn-primary" onClick={next}>
                Continuar <Icon.Arrow s={16}/>
              </button>
            </div>
          )}
          {step === 6 && (
            <div className="btn-row">
              <button className="btn btn-ghost" onClick={back} disabled={isPaying}><Icon.Back s={16}/> Atrás</button>
            </div>
          )}
        </div>
        <Summary data={data} distObj={distObj} precio={precio} step={step}/>
      </div>

      {showModal && (
        <DeclaracionModal
          onAccept={() => { set({ acepto: true }); setShowModal(false); setErrors(e => ({...e, acepto: undefined})); }}
          onClose={() => setShowModal(false)}/>
      )}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
