// src/screen-policies.jsx — Catálogo de PÓLIZAS.
//
// LISTA: filtros de ciclo de vida siempre visibles (patrón Applied Epic) y
// vencimiento en lenguaje humano.
//
// DETALLE (reacomodo 2026-09-06, decisión de Miguel): la ficha ya NO abre con
// los tres mosaicos de Vigencia / Prima total / Personas. Esa información la
// suplen las tres BARRAS que el agente pide de verdad, en este orden:
//
//   1. Cobranza    — cómo va el cobro de la póliza (la tira de recibos, #206).
//                    Colapsada: se ve el mapa de estatus; se pica para operar.
//   2. Contratante — quién es y quién paga.
//   3. Asegurados  — quién está cubierto.
//
// Son barras horizontales de ancho completo para que el mismo orden aguante en
// móvil apilándose. Lo demás (coberturas, financieros, datos, línea de tiempo,
// documentos) baja al bloque de abajo, del lado derecho y en colapsables.
// La versión anterior vive en _archive/catalogos-clasico-2026-07-06/.

function PoliciesScreen() {
  const { match } = useHashRoute();
  const detail = match('/polizas/:id');
  if (detail) return <PolicyDetail id={detail.params.id}/>;
  return <PolicyList/>;
}

const AX_VIGENTES_SET = new Set(['AUTO_APPROVED', 'MANUALLY_APPROVED', 'RENEWED', 'RENEWAL_PENDING']);

// ─────────────────────────────────────────────────────────────
// LISTA
// ─────────────────────────────────────────────────────────────
function PolicyList() {
  const { state } = useAxStore();
  const { navigate, query } = useHashRoute();
  const [cicloF, setCicloF] = useState(query.filter === 'renewal' ? 'renovar' : 'vigentes');
  const [carrierF, setCarrierF] = useState('all');
  const [ramoF, setRamoF] = useState('all');
  const [search, setSearch] = useState('');
  const [sortKey, setSortKey] = useState('finVigencia');
  const [vigF, setVigF] = useState('all');

  // Quién contrata cada póliza y cuántas personas toca (capa de personas).
  const metaPorPoliza = useMemo(() => {
    const m = {};
    state.policies.forEach((p) => {
      const parties = AdmixPersonas.partiesOfPolicy(p.id);
      const contratante = parties.find((x) => x.rol === 'CONTRATANTE');
      const personas = new Set(parties.map((x) => x.personaId));
      m[p.id] = { contratante: contratante ? contratante.persona : null, nPersonas: personas.size };
    });
    return m;
  }, [state.policies]);

  const counts = useMemo(() => {
    const c = { todas: state.policies.length, vigentes: 0, renovar: 0, historicas: 0 };
    state.policies.forEach((p) => {
      if (p.status === 'RENEWAL_PENDING') c.renovar++;
      if (AX_VIGENTES_SET.has(p.status)) c.vigentes++;
      else c.historicas++;
    });
    return c;
  }, [state.policies]);

  const primaLibro = useMemo(() =>
    state.policies.filter((p) => AX_VIGENTES_SET.has(p.status)).reduce((s, p) => s + p.primaTotal, 0),
  [state.policies]);

  const rows = useMemo(() => {
    let list = state.policies.slice();
    if (cicloF === 'vigentes')   list = list.filter((p) => AX_VIGENTES_SET.has(p.status));
    if (cicloF === 'renovar')    list = list.filter((p) => p.status === 'RENEWAL_PENDING');
    if (cicloF === 'historicas') list = list.filter((p) => !AX_VIGENTES_SET.has(p.status));
    if (carrierF !== 'all') list = list.filter((p) => p.carrier === carrierF);
    if (ramoF !== 'all')    list = list.filter((p) => p.ramo === ramoF);
    if (vigF === 'anual')    list = list.filter((p) => !formaVigencia(p).corta);
    if (vigF === 'noanual')  list = list.filter((p) => !!formaVigencia(p).corta);
    if (search) {
      const s = search.toLowerCase();
      list = list.filter((p) => {
        const k = metaPorPoliza[p.id] && metaPorPoliza[p.id].contratante;
        return [p.polizaNumero, p.producto, k ? k.nombre : '', k ? (k.rfc || '') : '']
          .some((v) => (v || '').toLowerCase().includes(s));
      });
    }
    list.sort((a, b) => {
      if (sortKey === 'finVigencia') return a.finVigencia.localeCompare(b.finVigencia);
      if (sortKey === 'createdAt')   return b.createdAt.localeCompare(a.createdAt);
      if (sortKey === 'primaTotal')  return b.primaTotal - a.primaTotal;
      if (sortKey === 'contratante') {
        const ka = metaPorPoliza[a.id] && metaPorPoliza[a.id].contratante;
        const kb = metaPorPoliza[b.id] && metaPorPoliza[b.id].contratante;
        return (ka ? ka.nombre : '').localeCompare(kb ? kb.nombre : '');
      }
      return 0;
    });
    return list;
  }, [state.policies, cicloF, carrierF, ramoF, vigF, search, sortKey, metaPorPoliza]);

  return (
    <div style={{ flex: 1, overflow: 'auto', display: 'flex', flexDirection: 'column' }}>
      <AxPageHeader
        eyebrow="Tu libro"
        title="Pólizas"
        subtitle={`${counts.vigentes} vigentes · ${AdmixData.formatMoney(primaLibro)} de prima anual en tu libro.`}
        actions={<AxButton variant="primary" icon="upload" onClick={() => navigate('/importar')}>Cargar pólizas</AxButton>}
      />

      <div style={{
        padding: '14px 32px', display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap',
        borderBottom: '1px solid var(--ax-border)', background: 'var(--ax-bg-1)',
      }}>
        <div style={{
          display: 'flex', alignItems: 'center', gap: 8,
          background: 'var(--ax-bg-4)', border: '1px solid var(--ax-border)',
          padding: '0 10px', height: 32, borderRadius: 6, flex: '0 0 250px',
        }}>
          <AxIcon name="search" size={13} style={{ color: 'var(--ax-fg-faint)' }}/>
          <input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Cliente, RFC, póliza, producto…"
            style={{ flex: 1, background: 'transparent', border: 0, outline: 'none', color: 'var(--ax-fg)', fontSize: 13 }}/>
        </div>
        <AxChipRow value={cicloF} onChange={setCicloF} options={[
          { v: 'vigentes',   label: 'Vigentes',    n: counts.vigentes },
          { v: 'renovar',    label: 'Por renovar', n: counts.renovar },
          { v: 'historicas', label: 'Históricas',  n: counts.historicas },
          { v: 'todas',      label: 'Todas',       n: counts.todas },
        ]}/>
        <div style={{ flex: 1 }}/>
        <FilterChip2 label="Aseguradora" value={carrierF} onChange={setCarrierF} options={[
          { v: 'all', label: 'Todas' }, ...AdmixData.CARRIERS.map((c) => ({ v: c.id, label: c.name })),
        ]}/>
        <FilterChip2 label="Ramo" value={ramoF} onChange={setRamoF} options={[
          { v: 'all', label: 'Todos' }, { v: 'GMM', label: 'Gastos Médicos' }, { v: 'VIDA', label: 'Vida' },
          { v: 'AUTOS', label: 'Autos' }, { v: 'DANOS', label: 'Daños' },
        ]}/>
        <FilterChip2 label="Vigencia" value={vigF} onChange={setVigF} options={[
          { v: 'all', label: 'Todas' },
          { v: 'anual', label: 'Anual' },
          { v: 'noanual', label: 'No anual' },
        ]}/>
        <FilterChip2 label="Orden" value={sortKey} onChange={setSortKey} options={[
          { v: 'finVigencia', label: 'Vence primero' },
          { v: 'createdAt',   label: 'Más recientes' },
          { v: 'primaTotal',  label: 'Prima (mayor)' },
          { v: 'contratante', label: 'Cliente (A→Z)' },
        ]}/>
        <span style={{ fontSize: 11, color: 'var(--ax-fg-faint)', fontFamily: 'var(--font-mono)' }}>
          {rows.length} de {counts.todas}
        </span>
      </div>

      <div style={{ flex: 1, overflow: 'auto' }}>
        <table className="ax-table">
          <thead>
            <tr>
              <th>Contratante</th>
              <th style={{ width: 120 }}>Aseguradora</th>
              <th style={{ width: 200 }}>Producto · Póliza</th>
              <th style={{ width: 90 }}>Ramo</th>
              <th style={{ width: 150 }}>Vigencia</th>
              <th style={{ width: 130, textAlign: 'right' }}>Prima total</th>
              <th style={{ width: 170 }}>Estado</th>
              <th style={{ width: 40 }}/>
            </tr>
          </thead>
          <tbody>
            {rows.map((p) => {
              const meta = metaPorPoliza[p.id] || {};
              const k = meta.contratante;
              const c = AdmixData.carrierById(p.carrier);
              return (
                <tr key={p.id} onClick={() => navigate(`/polizas/${p.id}`)}>
                  <td>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                      <AxAvatar name={k ? k.nombre : '?'} size={26}/>
                      <div style={{ minWidth: 0 }}>
                        <div style={{ fontSize: 13, color: 'var(--ax-fg-strong)', fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                          {k ? k.nombre : '—'}
                        </div>
                        <div style={{ fontSize: 10, color: 'var(--ax-fg-faint)' }}>
                          <span className="ax-mono">{k && k.rfc ? k.rfc : 'sin RFC'}</span>
                          {meta.nPersonas > 1 ? <span> · {meta.nPersonas} personas</span> : null}
                        </div>
                      </div>
                    </div>
                  </td>
                  <td>
                    <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
                      <AxCarrierAvatar id={p.carrier} size={20}/>
                      <span style={{ fontSize: 13 }}>{c ? c.name : '—'}</span>
                    </div>
                  </td>
                  <td>
                    <div style={{ fontSize: 13, color: 'var(--ax-fg-strong)' }}>{p.producto}</div>
                    <div className="ax-mono" style={{ fontSize: 10, color: 'var(--ax-fg-muted)' }}>{p.polizaNumero}</div>
                  </td>
                  <td><AxRamoTag code={p.ramo}/></td>
                  <td>
                    <AxVigenciaHumana iso={p.finVigencia}/>
                    {formaVigencia(p).corta ? (
                      <div className="ax-mono" style={{ fontSize: 9.5, color: 'var(--ax-info)', letterSpacing: '0.08em', marginTop: 2 }}>
                        {formaVigencia(p).corta}
                      </div>
                    ) : null}
                  </td>
                  <td style={{ textAlign: 'right' }}>
                    <span className="ax-mono" style={{ fontSize: 13, color: 'var(--ax-fg-strong)' }}>
                      {AdmixData.formatMoney(p.primaTotal)}
                    </span>
                  </td>
                  <td><AxStatusPill code={p.status}/></td>
                  <td><AxIcon name="chevron-right" size={13} style={{ color: 'var(--ax-fg-faint)' }}/></td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// DETALLE
// ─────────────────────────────────────────────────────────────

// Coberturas de muestra por ramo (la app real las trae de la extracción).
function coberturasDe(p) {
  if (p.ramo === 'GMM') return [
    { nombre: 'Suma asegurada',              tipo: 'Básica',    suma: '$ 50,000,000.00', ded: '$ 22,000.00', coa: '10 %' },
    { nombre: 'Tope de coaseguro',           tipo: 'Básica',    suma: '$ 120,000.00',    ded: '—',           coa: '—' },
    { nombre: 'Emergencia en el extranjero', tipo: 'Incluida',  suma: 'USD 100,000',     ded: 'USD 50',      coa: '—' },
    { nombre: 'Maternidad',                  tipo: 'Incluida',  suma: 'Amparada',        ded: '—',           coa: '—' },
    { nombre: 'Enfermedades catastróficas',  tipo: 'Incluida',  suma: 'Sin límite',      ded: '—',           coa: '—' },
  ];
  if (p.ramo === 'VIDA') return [
    { nombre: 'Fallecimiento',                 tipo: 'Básica',    suma: '$ 2,000,000.00', ded: '—', coa: '—' },
    { nombre: 'Invalidez total y permanente',  tipo: 'Adicional', suma: '$ 2,000,000.00', ded: '—', coa: '—' },
    { nombre: 'Muerte accidental (doble)',     tipo: 'Adicional', suma: '$ 4,000,000.00', ded: '—', coa: '—' },
    { nombre: 'Gastos funerarios',             tipo: 'Incluida',  suma: '$ 60,000.00',    ded: '—', coa: '—' },
  ];
  if (p.ramo === 'AUTOS') return [
    { nombre: 'Daños materiales',                tipo: 'Amplia',   suma: 'Valor comercial', ded: '5 %',  coa: '—' },
    { nombre: 'Robo total',                      tipo: 'Amplia',   suma: 'Valor comercial', ded: '10 %', coa: '—' },
    { nombre: 'Responsabilidad civil',           tipo: 'Básica',   suma: '$ 3,000,000.00',  ded: '—',    coa: '—' },
    { nombre: 'Gastos médicos a ocupantes',      tipo: 'Básica',   suma: '$ 500,000.00',    ded: '—',    coa: '—' },
    { nombre: 'Asistencia vial y legal',         tipo: 'Incluida', suma: 'Amparada',        ded: '—',    coa: '—' },
  ];
  return [
    { nombre: 'Incendio del inmueble',            tipo: 'Básica',    suma: '$ 4,500,000.00', ded: '—',   coa: '—' },
    { nombre: 'Contenidos',                        tipo: 'Básica',    suma: '$ 800,000.00',   ded: '1 %', coa: '—' },
    { nombre: 'Robo con violencia',                tipo: 'Adicional', suma: '$ 350,000.00',   ded: '5 %', coa: '—' },
    { nombre: 'Fenómenos hidrometeorológicos',     tipo: 'Adicional', suma: '$ 4,500,000.00', ded: '2 %', coa: '—' },
    { nombre: 'Responsabilidad civil familiar',    tipo: 'Incluida',  suma: '$ 1,000,000.00', ded: '—',   coa: '—' },
  ];
}

// ─────────────────────────────────────────────────────────────
// COBRANZA — la tira de recibos (#206)
//
// El motor ya guarda cada recibo de cada póliza (número, estado canónico,
// vencimiento, importe, fracción) y la ficha sólo enseñaba la píldora de
// vigencia. La tira convierte esos renglones en la barra de la vigencia: una
// píldora por recibo —una anual, dos semestrales, doce mensuales— del color de
// su estado y con su monto.
//
// UNA TIRA POR VIGENCIA (Miguel, 2026-09-06). Casi siempre la vigencia es
// anual, y hay dos excepciones que él nombró:
//   · autos BIANUALES — la vigencia corre 24 meses, la tira también.
//   · vida tipo Consolida Total — la vigencia va del inicio al fin del PERIODO
//     DE PROTECCIÓN, no al año. Cuando esa tira pasaría de 26 píldoras, cada
//     píldora se vuelve una ANUALIDAD (con el peor estado de sus recibos
//     adentro) para que la barra siga siendo legible de un vistazo.
// Las vigencias anteriores no se pierden: bajan a un renglón por vigencia con
// su tira en miniatura, y se abren al picarlas.
//
// Dos reglas de lectura, para que siete señales no se vuelvan ruido:
//   · el COLOR dice el DINERO      (cobrado · en gracia · vencido · por cobrar)
//   · la TEXTURA dice la PROCEDENCIA (punteado = tu registro, el portal
//     todavía no lo confirma; hueco = recibo que no hemos leído)
//
// Ninguna señal se calcula en la pantalla en la app real: igual que la píldora
// de vigencia, todas bajan derivadas del motor. Aquí se derivan para que la
// maqueta tenga qué enseñar.
// ─────────────────────────────────────────────────────────────

const RECIBOS_POR_FORMA = { Anual: 1, Semestral: 2, Trimestral: 4, Mensual: 12 };
const RECIBOS_FORMA_LABEL = { Anual: 'anual', Semestral: 'semestrales', Trimestral: 'trimestrales', Mensual: 'mensuales' };
const GRACIA_DIAS = 30;
const TIRA_MAX_PILDORAS = 26;

const RECIBO_ESTADO = {
  pagado:     { etiqueta: 'Cobrado',         marca: '✓', linea: 'var(--ax-accent-line)',   fondo: 'var(--ax-accent-soft)', texto: 'var(--ax-accent)' },
  registrado: { etiqueta: 'Cobrado por ti',  marca: '◷', linea: 'var(--ax-accent-line)',   fondo: 'var(--ax-accent-soft)', texto: 'var(--ax-accent)', punteado: true },
  gracia:     { etiqueta: 'En gracia',       marca: '!', linea: 'var(--ax-warm-line)',     fondo: 'var(--ax-warm-soft)',   texto: 'var(--ax-warm)' },
  vencido:    { etiqueta: 'Vencido',         marca: '✕', linea: 'rgba(233,76,77,0.30)',    fondo: 'var(--ax-danger-soft)', texto: 'var(--ax-danger)' },
  porcobrar:  { etiqueta: 'Por cobrar',      marca: '',  linea: 'var(--ax-border-strong)', fondo: 'var(--ax-bg-3)',        texto: 'var(--ax-fg-faint)' },
  cancelado:  { etiqueta: 'Cancelado',       marca: '—', linea: 'var(--ax-border)',        fondo: 'transparent',           texto: 'var(--ax-fg-faint)', tachado: true },
  hueco:      { etiqueta: 'No leído',        marca: '?', linea: 'var(--ax-border-strong)', fondo: 'transparent',           texto: 'var(--ax-fg-faint)', punteado: true },
};

const RECIBO_MESES = ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'];

function reciboHash(s) {
  let h = 0;
  for (let i = 0; i < s.length; i++) h = ((h * 31 + s.charCodeAt(i)) >>> 0);
  return h;
}

function reciboSumaMeses(iso, meses) {
  const partes = iso.split('-').map(Number);
  const d = new Date(Date.UTC(partes[0], partes[1] - 1 + meses, 1));
  const ultimoDia = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 0)).getUTCDate();
  d.setUTCDate(Math.min(partes[2], ultimoDia));
  return d.toISOString().slice(0, 10);
}

function reciboPeriodo(iso, meses) {
  const partes = iso.split('-').map(Number);
  const mes = partes[1] - 1;
  if (meses >= 12) return RECIBO_MESES[mes] + ' ' + partes[0];
  if (meses === 1) return RECIBO_MESES[mes];
  return RECIBO_MESES[mes] + '→' + RECIBO_MESES[(mes + meses - 1) % 12];
}

// Cuánto dura la vigencia de esta póliza y cómo se llama.
function formaVigencia(p) {
  const h = reciboHash(p.id);
  if (p.ramo === 'AUTOS' && h % 13 === 0) {
    return { meses: 24, nota: 'Vigencia bianual', corta: 'Bianual' };
  }
  if (p.ramo === 'VIDA' && h % 11 === 0) {
    const anios = 10 + (h % 3) * 5;
    const fin = Number(p.inicioVigencia.slice(0, 4)) + anios;
    return {
      meses: anios * 12,
      nota: 'Vigencia hasta el fin del periodo de protección · ' + fin,
      corta: 'Protección → ' + fin,
    };
  }
  return { meses: 12, nota: null, corta: null };
}

// Los recibos de UNA vigencia, como los devolvería el motor: ordenados por
// vencimiento y con su estado ya derivado contra hoy y los días de gracia.
function recibosDeVigencia(p, desde, meses, actual) {
  const h = reciboHash(p.id);
  const porAnio = RECIBOS_POR_FORMA[p.formaPago] || 1;
  let mesesPorRecibo = 12 / porAnio;
  let agregado = false;
  if (meses / mesesPorRecibo > TIRA_MAX_PILDORAS) { mesesPorRecibo = 12; agregado = true; }

  const n = Math.max(1, Math.round(meses / mesesPorRecibo));
  const importe = p.primaTotal * (mesesPorRecibo / 12);
  const caida = p.status === 'LAPSED' || p.status === 'CANCELLED';

  const lista = [];
  for (let i = 0; i < n; i++) {
    const vence = reciboSumaMeses(desde, mesesPorRecibo * i);
    lista.push({
      numero: p.polizaNumero + '-' + String(i + 1).padStart(2, '0') + (actual ? '' : 'H'),
      fraccion: (i + 1) + '/' + n,
      periodo: agregado ? 'Año ' + (i + 1) : reciboPeriodo(vence, mesesPorRecibo),
      resume: agregado ? porAnio + ' recibos ' + (RECIBOS_FORMA_LABEL[p.formaPago] || '') : null,
      vence,
      dias: AdmixPersonas.daysUntil(vence),
      importe,
      formaPago: p.formaPago === 'Anual' ? 'Anual' : (h % 3 === 0 ? 'Transferencia' : 'Cargo automático'),
      canal: h % 3 === 0 ? 'Intermediario' : 'Banco',
      subestado: null,
      fuente: 'Portal ' + ((AdmixData.carrierById(p.carrier) || {}).name || '') + ' · última lectura',
      estado: 'porcobrar',
    });
  }

  // Una vigencia ya cerrada quedó cobrada: si no, la póliza no habría seguido.
  if (!actual) {
    lista.forEach((r) => { r.estado = 'pagado'; });
    return lista;
  }

  // El último ya vencido es el que decide; los anteriores están cobrados.
  let ultimoVencido = -1;
  lista.forEach((r, i) => { if (r.dias <= 0) ultimoVencido = i; });
  lista.forEach((r, i) => { r.estado = i < ultimoVencido ? 'pagado' : 'porcobrar'; });

  if (ultimoVencido >= 0) {
    const r = lista[ultimoVencido];
    const atraso = -r.dias;
    if (caida) {
      r.estado = 'vencido';
    } else if (atraso > GRACIA_DIAS * 2) {
      // Venció hace mucho y la póliza sigue en pie: está cobrado.
      r.estado = 'pagado';
    } else {
      const d = h % 100;
      if (d < 52) r.estado = 'pagado';
      else if (d < 68) { r.estado = 'registrado'; r.fuente = 'Tu registro de cobro · el portal aún no lo refleja'; }
      else if (d < 88 && atraso <= GRACIA_DIAS) { r.estado = 'gracia'; if (h % 5 === 0) r.subestado = 'CB · rechazo bancario'; }
      else if (atraso > 0) r.estado = 'vencido';
      else r.estado = 'pagado';
    }
  }

  // Un recibo que el portal no nos entregó: el hueco se enseña, porque
  // "no sabemos" no es lo mismo que "no debe".
  if (n > 1 && h % 17 === 0) {
    const ultimo = lista[lista.length - 1];
    ultimo.estado = 'hueco';
    ultimo.numero = '—';
    ultimo.fuente = 'La fracción dice ' + n + ' y el portal nos dio ' + (n - 1);
  }

  // Cancelado y reexpedido: se ve, se tacha y no cuenta en el saldo.
  if (h % 23 === 0 && ultimoVencido >= 0) {
    const base = lista[ultimoVencido];
    lista.splice(ultimoVencido, 0, Object.assign({}, base, {
      numero: base.numero + 'C',
      estado: 'cancelado',
      subestado: 'Reexpedido',
    }));
  }

  return lista;
}

// Las vigencias de la póliza, de la más reciente hacia atrás.
function vigenciasDe(p) {
  const h = reciboHash(p.id);
  const forma = formaVigencia(p);

  // 12 de cada 193 pólizas del libro real no tienen ni un recibo leído.
  // Eso también es información: la tira no se inventa una.
  if (h % 29 === 0) return [];

  const previas = forma.meses > 12 ? 0
    : (p.status === 'RENEWED' || p.status === 'RENEWAL_PENDING') ? 1 + (h % 2)
    : (h % 2);
  const lista = [];
  for (let k = previas; k >= 0; k--) {
    const desde = reciboSumaMeses(p.inicioVigencia, -forma.meses * k);
    const hasta = reciboSumaMeses(desde, forma.meses);
    lista.push({
      id: 'v' + k,
      actual: k === 0,
      desde,
      hasta,
      etiqueta: desde.slice(0, 4) + (forma.meses > 12 ? '→' + hasta.slice(0, 4) : ''),
      nota: forma.nota,
      recibos: recibosDeVigencia(p, desde, forma.meses, k === 0),
    });
  }
  return lista.reverse();
}

function saldoDe(recibos) {
  const s = { cobrado: 0, porCobrar: 0, sinLeer: 0, total: 0, cobrados: 0, cuenta: 0 };
  recibos.forEach((r) => {
    if (r.estado === 'cancelado') return;
    s.cuenta += 1;
    if (r.estado === 'hueco') { s.sinLeer += 1; return; }
    s.total += r.importe;
    if (r.estado === 'pagado' || r.estado === 'registrado') { s.cobrado += r.importe; s.cobrados += 1; }
    else s.porCobrar += r.importe;
  });
  return s;
}

// La barra de una vigencia: una píldora por recibo. El recibo que sostiene la
// vigencia sale seleccionado de entrada; no lleva rótulo — el veredicto ya se
// dice arriba, en la píldora de la cabecera.
function TiraDeVigencia({ recibos, iSel, decisor, onPick }) {
  return (
    <div style={{ display: 'flex', gap: 4, overflowX: 'auto', paddingTop: 14, paddingBottom: 6 }}>
      {recibos.map((r, i) => {
        const t = RECIBO_ESTADO[r.estado];
        const activo = i === iSel;
        const marcaHoy = r.dias > 0 && i > 0 && recibos[i - 1].dias <= 0;
        return (
          <div key={r.numero + i} style={{ display: 'contents' }}>
            {marcaHoy ? (
              <div style={{ position: 'relative', width: 1, alignSelf: 'stretch', margin: '0 4px', background: 'var(--ax-info)', opacity: 0.6, flexShrink: 0 }}>
                <span className="ax-mono" style={{
                  position: 'absolute', top: -13, left: '50%', transform: 'translateX(-50%)',
                  fontSize: 8, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--ax-info)',
                }}>hoy</span>
              </div>
            ) : null}
            <button
              onClick={() => onPick(i)}
              title={t.etiqueta + ' · vence ' + AdmixData.formatDateES(r.vence)}
              style={{
                position: 'relative', flex: '1 1 0', minWidth: 58,
                appearance: 'none', cursor: 'default', textAlign: 'center',
                display: 'flex', flexDirection: 'column', gap: 4,
                padding: '8px 6px 7px', borderRadius: 7,
                border: '1px ' + (t.punteado ? 'dashed ' : 'solid ') + (activo ? 'var(--ax-info)' : t.linea),
                boxShadow: activo ? '0 0 0 1px var(--ax-info)' : 'none',
                background: t.fondo, color: t.texto, fontFamily: 'var(--font-mono)',
              }}
            >
              <span style={{ fontSize: 10, lineHeight: 1, height: 10 }}>{t.marca}</span>
              <span style={{ fontSize: 9, letterSpacing: '0.10em', textTransform: 'uppercase', opacity: 0.85 }}>{r.periodo}</span>
              <span style={{
                fontSize: 11, fontWeight: 600, fontVariantNumeric: 'tabular-nums',
                textDecoration: t.tachado ? 'line-through' : 'none',
              }}>
                {r.estado === 'hueco' ? '—' : Math.round(r.importe).toLocaleString('en-US')}
              </span>
            </button>
          </div>
        );
      })}
    </div>
  );
}

// Tira en miniatura, para el renglón de una vigencia cerrada.
function TiraMini({ recibos }) {
  return (
    <div style={{ display: 'flex', gap: 2, flex: '1 1 120px', minWidth: 80 }}>
      {recibos.map((r, i) => (
        <span key={i} style={{
          flex: '1 1 0', height: 5, borderRadius: 2,
          background: RECIBO_ESTADO[r.estado].texto, opacity: 0.55,
        }}/>
      ))}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// BARRA 1 — COBRANZA
// ─────────────────────────────────────────────────────────────
function BarraCobranza({ p, abierta, onToggle }) {
  const vigencias = useMemo(() => vigenciasDe(p), [p.id]);
  const [vSel, setVSel] = useState(0);
  const [registrados, setRegistrados] = useState([]);
  const [sel, setSel] = useState(null);

  const vig = vigencias[vSel] || vigencias[0] || null;

  const recibos = useMemo(() => (vig ? vig.recibos.map((r) => (
    registrados.indexOf(r.numero) >= 0
      ? Object.assign({}, r, { estado: 'registrado', subestado: null, fuente: 'Tu registro de cobro · el portal aún no lo refleja' })
      : r
  )) : []), [vig, registrados]);

  // El recibo que sostiene la vigencia: el último vencido que no está
  // cancelado. Si ninguno ha vencido, manda el primero.
  const decisor = useMemo(() => {
    let idx = -1;
    recibos.forEach((r, i) => { if (r.dias <= 0 && r.estado !== 'cancelado' && r.estado !== 'hueco') idx = i; });
    if (idx < 0 && recibos.length) idx = 0;
    return idx;
  }, [recibos]);

  const saldo = useMemo(() => saldoDe(recibos), [recibos]);
  const iSel = sel != null && recibos[sel] ? sel : decisor;
  const rSel = recibos[iSel] || null;

  if (vigencias.length === 0) {
    return (
      <BarraFicha
        titulo="Cobranza"
        resumen="Sin recibos leídos"
        derecha={<AxPill tone="ghost">Vigencia desconocida</AxPill>}
        abierta={abierta}
        onToggle={onToggle}
      >
        <div style={{ display: 'flex', gap: 10, alignItems: 'flex-start', color: 'var(--ax-fg-muted)', fontSize: 12.5 }}>
          <AxIcon name="info" size={14} style={{ color: 'var(--ax-fg-faint)', flexShrink: 0, marginTop: 2 }}/>
          <div>
            Todavía no hemos leído ningún recibo de esta póliza, así que no podemos decir qué está cobrado.
            <div style={{ fontSize: 11.5, color: 'var(--ax-fg-faint)', marginTop: 3 }}>
              Trae los datos de la aseguradora para llenarla.
            </div>
          </div>
        </div>
      </BarraFicha>
    );
  }

  // El veredicto de vigencia sale del recibo que decide — la misma regla del
  // motor, dicha en una píldora.
  const rDec = recibos[decisor];
  const veredicto = !rDec ? { t: 'ghost', l: 'Vigencia desconocida' }
    : rDec.estado === 'vencido' ? { t: 'danger', l: 'No vigente' }
    : rDec.estado === 'gracia' ? { t: 'warm', l: 'Vigente · en gracia' }
    : { t: 'accent', l: 'Vigente' };

  const nPildoras = recibos.filter((r) => r.estado !== 'cancelado').length;
  const forma = RECIBOS_FORMA_LABEL[p.formaPago] || '';
  const agregada = recibos.length > 0 && !!recibos[0].resume;

  return (
    <BarraFicha
      titulo="Cobranza"
      resumen={
        (agregada ? nPildoras + ' anualidades' : nPildoras + (nPildoras === 1 ? ' recibo ' : ' recibos ') + forma)
        + ' · ' + AdmixData.formatDateES(vig.desde) + ' → ' + AdmixData.formatDateES(vig.hasta)
        + (vig.nota ? ' · ' + vig.nota : '')
      }
      derecha={
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
          <span className="ax-mono" style={{ fontSize: 11.5, color: 'var(--ax-fg-muted)', textAlign: 'right' }}>
            <span style={{ color: 'var(--ax-accent)', fontWeight: 600 }}>{AdmixData.formatMoney(saldo.cobrado)} cobrado</span>
            {saldo.porCobrar > 0 ? <span> · <span style={{ color: 'var(--ax-warm)', fontWeight: 600 }}>{AdmixData.formatMoney(saldo.porCobrar)} por cobrar</span></span> : null}
            {saldo.sinLeer > 0 ? <span> · {saldo.sinLeer} sin leer</span> : null}
          </span>
          <AxPill tone={veredicto.t}>{veredicto.l}</AxPill>
        </div>
      }
      cabecera={
        <div style={{ padding: '0 20px' }}>
          <TiraDeVigencia recibos={recibos} iSel={abierta ? iSel : -1} decisor={decisor}
            onPick={(i) => { setSel(i); if (!abierta) onToggle(); }}/>
        </div>
      }
      abierta={abierta}
      onToggle={onToggle}
    >
      {/* Detalle del recibo elegido + la palanca de nivel 3 */}
      {rSel ? (
        <div style={{ display: 'flex', gap: 18, alignItems: 'flex-start', flexWrap: 'wrap' }}>
          <div style={{ flex: '1 1 420px', minWidth: 0, display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))', gap: 12 }}>
            <Kv label="Recibo" mono>{rSel.numero}</Kv>
            <Kv label="Estado">{RECIBO_ESTADO[rSel.estado].etiqueta}</Kv>
            <Kv label="Vence" mono>{rSel.estado === 'hueco' ? '—' : AdmixData.formatDateES(rSel.vence)}</Kv>
            <Kv label="Importe" mono>{rSel.estado === 'hueco' ? '—' : AdmixData.formatMoney(rSel.importe)}</Kv>
            <Kv label="Fracción" mono>{rSel.resume ? rSel.resume : rSel.fraccion}</Kv>
            <Kv label="Forma de pago">{rSel.estado === 'hueco' ? '—' : rSel.formaPago}</Kv>
            <Kv label="Canal de cobro">{rSel.estado === 'hueco' ? '—' : rSel.canal}</Kv>
            <Kv label="Subestado">{rSel.subestado || '—'}</Kv>
            <Kv label="Según" span2>{rSel.fuente}</Kv>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6, alignItems: 'flex-end' }}>
            {rSel.estado === 'gracia' || rSel.estado === 'vencido' || rSel.estado === 'porcobrar' ? (
              <AxButton variant="primary" onClick={() => setRegistrados(registrados.concat([rSel.numero]))}>
                Registrar cobro
              </AxButton>
            ) : (
              <span className="ax-mono" style={{
                fontSize: 10.5, letterSpacing: '0.10em', textTransform: 'uppercase',
                color: 'var(--ax-fg-faint)', border: '1px solid var(--ax-border)',
                borderRadius: 6, padding: '9px 14px', whiteSpace: 'nowrap',
              }}>Sin cobro que registrar</span>
            )}
            <span style={{ fontSize: 10.5, color: 'var(--ax-fg-faint)', maxWidth: 190, textAlign: 'right' }}>
              Tu registro cuenta como pagado desde ya; el portal lo confirma después.
            </span>
          </div>
        </div>
      ) : null}

      {/* Leyenda — el color dice el dinero, la textura la procedencia */}
      <div style={{ marginTop: 16, display: 'flex', gap: 14, flexWrap: 'wrap', alignItems: 'center', fontSize: 10.5, color: 'var(--ax-fg-faint)' }}>
        {['pagado', 'registrado', 'gracia', 'vencido', 'porcobrar'].map((k) => (
          <span key={k} style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
            <span style={{
              width: 10, height: 10, borderRadius: 3,
              border: '1px ' + (RECIBO_ESTADO[k].punteado ? 'dashed ' : 'solid ') + RECIBO_ESTADO[k].linea,
              background: RECIBO_ESTADO[k].fondo,
            }}/>
            {RECIBO_ESTADO[k].etiqueta}
          </span>
        ))}
      </div>

      {/* Vigencias anteriores — una tira por vigencia, plegada */}
      {vigencias.length > 1 ? (
        <div style={{ marginTop: 18, borderTop: '1px solid var(--ax-border)', paddingTop: 14 }}>
          <AxEyebrow style={{ marginBottom: 8 }}>Vigencias</AxEyebrow>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
            {vigencias.map((v, i) => {
              const s = saldoDe(v.recibos);
              const activa = i === vSel;
              return (
                <button key={v.id}
                  onClick={() => { setVSel(i); setSel(null); }}
                  style={{
                    appearance: 'none', cursor: 'default', textAlign: 'left', color: 'inherit',
                    background: activa ? 'var(--ax-bg-3)' : 'transparent',
                    border: '1px solid ' + (activa ? 'var(--ax-border-strong)' : 'transparent'),
                    borderRadius: 6, padding: '8px 10px',
                    display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap',
                  }}
                >
                  <span className="ax-mono" style={{ fontSize: 11, color: 'var(--ax-fg-strong)', minWidth: 74 }}>
                    {v.etiqueta}{v.actual ? ' · en curso' : ''}
                  </span>
                  <TiraMini recibos={v.recibos}/>
                  <span className="ax-mono" style={{ fontSize: 11, color: 'var(--ax-fg-muted)' }}>
                    {s.cobrados} de {s.cuenta} cobrados
                  </span>
                  <span className="ax-mono" style={{ fontSize: 11, color: 'var(--ax-fg-faint)' }}>
                    {AdmixData.formatMoney(s.total)}
                  </span>
                </button>
              );
            })}
          </div>
        </div>
      ) : null}
    </BarraFicha>
  );
}

// ─────────────────────────────────────────────────────────────
// BARRA 2 — CONTRATANTE
// ─────────────────────────────────────────────────────────────
function BarraContratante({ p, contratante, abierta, onToggle }) {
  const { navigate } = useHashRoute();
  if (!contratante) {
    return (
      <BarraFicha titulo="Contratante" resumen="Sin contratante identificado" abierta={abierta} onToggle={onToggle}>
        <div style={{ fontSize: 12.5, color: 'var(--ax-fg-muted)' }}>
          Esta póliza no tiene todavía una persona ligada como contratante.
        </div>
      </BarraFicha>
    );
  }

  const kyc = AdmixPersonas.kycDe(contratante.id);
  const prima = AdmixPersonas.primaACargo(contratante.id);
  const cartera = AdmixPersonas.policiesOfPersona(contratante.id).filter((r) => r.obligadoPago);
  const dir = contratante.direccion;

  return (
    <BarraFicha
      titulo="Contratante"
      icono={<AxAvatar name={contratante.nombre} size={30}/>}
      resumen={
        <span>
          <span style={{ color: 'var(--ax-fg-strong)', fontWeight: 500 }}>{contratante.nombre}</span>
          <span className="ax-mono" style={{ marginLeft: 8, fontSize: 11, color: 'var(--ax-fg-faint)' }}>
            {contratante.rfc || 'sin RFC'}
          </span>
        </span>
      }
      derecha={
        <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
          <AxPill tone="accent" dot>Paga la prima</AxPill>
          {contratante.tipo === 'moral' ? <AxPill tone="neutral">Empresa</AxPill> : null}
          {kyc && kyc.estado !== 'ok' ? <AxPill tone="warm">{kyc.etiqueta}</AxPill> : null}
        </div>
      }
      abierta={abierta}
      onToggle={onToggle}
    >
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(160px, 1fr))', gap: 14 }}>
        <Kv label="Correo">{contratante.email || '—'}</Kv>
        <Kv label="Teléfono" mono>{contratante.telefono || '—'}</Kv>
        <Kv label={contratante.tipo === 'moral' ? 'RFC' : 'CURP'} mono>
          {contratante.tipo === 'moral' ? (contratante.rfc || '—') : (contratante.curp || '—')}
        </Kv>
        <Kv label="Identificación">{kyc ? kyc.etiqueta : '—'}</Kv>
        <Kv label="Domicilio" span2>
          {dir ? dir.calle + ', ' + dir.colonia + ', ' + dir.cp + ' ' + dir.ciudad + ', ' + dir.estado : '—'}
        </Kv>
        {contratante.repLegal ? <Kv label="Representante legal">{contratante.repLegal}</Kv> : null}
        <Kv label="Prima a su cargo" mono>{AdmixData.formatMoney(prima)}</Kv>
        <Kv label="Pólizas que paga" mono>{cartera.length}</Kv>
      </div>
      <div style={{ marginTop: 14 }}>
        <AxButton variant="secondary" icon="chevron-right"
          onClick={() => navigate(AdmixPersonas.conOrigen('/clientes/' + contratante.id, '/polizas/' + p.id, p.producto))}>
          Abrir su ficha
        </AxButton>
      </div>
    </BarraFicha>
  );
}

// ─────────────────────────────────────────────────────────────
// BARRA 3 — ASEGURADOS (y la 4ª, beneficiarios, cuando los hay)
// ─────────────────────────────────────────────────────────────
function BarraPersonas({ p, titulo, gente, vacio, mostrarPorcentaje, abierta, onToggle }) {
  const { navigate } = useHashRoute();
  if (gente.length === 0) {
    return (
      <BarraFicha titulo={titulo} resumen={vacio} abierta={abierta} onToggle={onToggle}>
        <div style={{ fontSize: 12.5, color: 'var(--ax-fg-muted)' }}>{vacio}</div>
      </BarraFicha>
    );
  }

  const titular = gente.find((e) => e.titular);
  const menores = gente.filter((e) => {
    const a = AdmixPersonas.edad(e.persona.fechaNacimiento);
    return a != null && a < 18;
  }).length;

  return (
    <BarraFicha
      titulo={titulo}
      icono={
        <div style={{ display: 'flex' }}>
          {gente.slice(0, 4).map((e, i) => (
            <span key={e.persona.id} style={{ marginLeft: i === 0 ? 0 : -8, borderRadius: '50%', border: '2px solid var(--ax-bg-2)', display: 'inline-flex' }}>
              <AxAvatar name={e.persona.nombre} size={26}/>
            </span>
          ))}
        </div>
      }
      resumen={
        <span>
          <span style={{ color: 'var(--ax-fg-strong)', fontWeight: 500 }}>
            {gente.length} {gente.length === 1 ? titulo.replace(/s$/, '').toLowerCase() : titulo.toLowerCase()}
          </span>
          {titular ? <span> · titular {titular.persona.nombre.split(/\s+/).slice(0, 2).join(' ')}</span> : null}
          {mostrarPorcentaje ? <span> · {gente.map((e) => (e.porcentaje != null ? e.porcentaje + ' %' : '—')).join(' / ')}</span> : null}
        </span>
      }
      derecha={menores > 0 ? <AxPill tone="info">{menores} menor{menores > 1 ? 'es' : ''} de edad</AxPill> : null}
      abierta={abierta}
      onToggle={onToggle}
    >
      <div style={{ border: '1px solid var(--ax-border)', borderRadius: 8, overflow: 'hidden' }}>
        {gente.map((e, i) => {
          const kyc = AdmixPersonas.kycDe(e.persona.id);
          const anios = AdmixPersonas.edad(e.persona.fechaNacimiento);
          return (
            <button key={e.persona.id}
              onClick={() => navigate(AdmixPersonas.conOrigen('/clientes/' + e.persona.id, '/polizas/' + p.id, p.producto))}
              style={{
                appearance: 'none', background: 'transparent', border: 0, color: 'inherit',
                width: '100%', textAlign: 'left', cursor: 'default',
                padding: '12px 14px', borderTop: i > 0 ? '1px solid var(--ax-border)' : 'none',
                display: 'flex', alignItems: 'center', gap: 12, flexWrap: 'wrap',
              }}
              onMouseEnter={(ev) => ev.currentTarget.style.background = 'var(--ax-bg-3)'}
              onMouseLeave={(ev) => ev.currentTarget.style.background = 'transparent'}
            >
              <AxAvatar name={e.persona.nombre} size={30}/>
              <div style={{ flex: '1 1 200px', minWidth: 0 }}>
                <div style={{ fontSize: 13, color: 'var(--ax-fg-strong)', fontWeight: 500, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
                  <span>{e.persona.nombre}</span>
                  {e.persona.tipo === 'moral' ? <AxPill tone="neutral">Empresa</AxPill> : null}
                  {anios != null && anios < 18 ? <span style={{ fontSize: 11, color: 'var(--ax-fg-muted)' }}>{anios} años</span> : null}
                </div>
                <div style={{ fontSize: 11, color: 'var(--ax-fg-muted)', marginTop: 2 }}>
                  {[
                    e.parentesco,
                    kyc && kyc.estado !== 'ok' ? 'identificación: ' + kyc.etiqueta.toLowerCase() : null,
                  ].filter(Boolean).join(' · ') || (e.persona.rfc ? e.persona.rfc : 'sin RFC')}
                </div>
              </div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 6, flexWrap: 'wrap', justifyContent: 'flex-end' }}>
                {e.porcentaje != null ? <AxPill tone="info">{e.porcentaje} % del beneficio</AxPill> : null}
                {e.titular ? <AxPill tone="neutral">Titular</AxPill> : null}
                <AxIcon name="chevron-right" size={13} style={{ color: 'var(--ax-fg-faint)' }}/>
              </div>
            </button>
          );
        })}
      </div>
    </BarraFicha>
  );
}

// ─────────────────────────────────────────────────────────────
// La barra: cabecera siempre visible + cuerpo colapsable.
// El contenido de `cabecera` se ve TAMBIÉN cerrada — ahí vive la tira, que es
// justo lo que el agente quiere de un vistazo sin abrir nada.
// ─────────────────────────────────────────────────────────────
function BarraFicha({ titulo, icono, resumen, derecha, cabecera, abierta, onToggle, children }) {
  return (
    <div className="ax-card" style={{ padding: 0, overflow: 'hidden' }}>
      <button
        onClick={onToggle}
        style={{
          appearance: 'none', background: 'transparent', border: 0, color: 'inherit',
          width: '100%', textAlign: 'left', cursor: 'default',
          padding: '14px 20px', display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap',
        }}
        onMouseEnter={(e) => e.currentTarget.style.background = 'var(--ax-bg-3)'}
        onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
      >
        {icono || null}
        <div style={{ flex: '1 1 240px', minWidth: 0 }}>
          <AxEyebrow>{titulo}</AxEyebrow>
          <div style={{ fontSize: 12.5, color: 'var(--ax-fg-muted)', marginTop: 3 }}>{resumen}</div>
        </div>
        {derecha ? <div style={{ flex: '0 1 auto' }}>{derecha}</div> : null}
        <AxIcon name={abierta ? 'chevron-down' : 'chevron-right'} size={14} style={{ color: 'var(--ax-fg-faint)', flexShrink: 0 }}/>
      </button>
      {cabecera || null}
      {abierta ? <div style={{ padding: '4px 20px 20px' }}>{children}</div> : null}
    </div>
  );
}

// Tarjeta clásica, para el bloque de abajo.
function PolicyCard({ title, eyebrow, actions, children, compact }) {
  return (
    <div className="ax-card" style={{ padding: compact ? 16 : 20 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 14, gap: 12 }}>
        <div>
          {eyebrow ? <AxEyebrow style={{ marginBottom: 4 }}>{eyebrow}</AxEyebrow> : null}
          <h3 style={{ margin: 0, fontSize: 15, fontWeight: 500, color: 'var(--ax-fg-strong)' }}>{title}</h3>
        </div>
        {actions}
      </div>
      {children}
    </div>
  );
}

// Tarjeta plegable, para lo de consulta (datos, línea de tiempo, documentos).
function PolicyCardPlegable({ title, resumen, abierta, onToggle, children }) {
  return (
    <div className="ax-card" style={{ padding: 0, overflow: 'hidden' }}>
      <button
        onClick={onToggle}
        style={{
          appearance: 'none', background: 'transparent', border: 0, color: 'inherit',
          width: '100%', textAlign: 'left', cursor: 'default',
          padding: '13px 16px', display: 'flex', alignItems: 'center', gap: 10,
        }}
        onMouseEnter={(e) => e.currentTarget.style.background = 'var(--ax-bg-3)'}
        onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
      >
        <div style={{ flex: 1, minWidth: 0 }}>
          <h3 style={{ margin: 0, fontSize: 14, fontWeight: 500, color: 'var(--ax-fg-strong)' }}>{title}</h3>
          {resumen ? <div style={{ fontSize: 11.5, color: 'var(--ax-fg-faint)', marginTop: 2 }}>{resumen}</div> : null}
        </div>
        <AxIcon name={abierta ? 'chevron-down' : 'chevron-right'} size={13} style={{ color: 'var(--ax-fg-faint)' }}/>
      </button>
      {abierta ? <div style={{ padding: '0 16px 16px' }}>{children}</div> : null}
    </div>
  );
}

function PolicyDetail({ id }) {
  const { state } = useAxStore();
  const { navigate, query } = useHashRoute();
  const p = state.policies.find((x) => x.id === id);

  // Qué barra está abierta. Arrancan TODAS cerradas: el agente ve el mapa de
  // cobranza y los nombres, y abre sólo lo que va a operar.
  const [abierta, setAbierta] = useState(null);
  const [datosAbiertos, setDatosAbiertos] = useState(null);
  const toggle = (k) => setAbierta(abierta === k ? null : k);
  const toggleDatos = (k) => setDatosAbiertos(datosAbiertos === k ? null : k);

  const parties = useMemo(() => (p ? AdmixPersonas.partiesOfPolicy(p.id) : []), [p && p.id]);

  // Personas únicas con sus roles agregados (una fila por persona, no por rol).
  const porRol = useMemo(() => {
    const porPersona = {};
    parties.forEach((x) => {
      if (!x.persona) return;
      porPersona[x.personaId] = porPersona[x.personaId] || { persona: x.persona, roles: [], obligadoPago: false, parentesco: null, porcentaje: null, titular: false };
      const e = porPersona[x.personaId];
      e.roles.push(x.rol);
      if (x.obligadoPago) e.obligadoPago = true;
      if (x.parentesco) e.parentesco = x.parentesco;
      if (x.porcentaje != null) e.porcentaje = x.porcentaje;
      if (x.titular) e.titular = true;
    });
    const todas = Object.values(porPersona);
    return {
      asegurados: todas.filter((e) => e.roles.includes('ASEGURADO')).sort((a, b) => (b.titular ? 1 : 0) - (a.titular ? 1 : 0)),
      beneficiarios: todas.filter((e) => e.roles.includes('BENEFICIARIO')),
    };
  }, [parties]);

  if (!p) {
    return <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <AxEmpty icon="book" title="Póliza no encontrada" action={<AxButton variant="secondary" onClick={() => navigate('/polizas')}>Volver</AxButton>}/>
    </div>;
  }

  const origen = AdmixPersonas.origenDe(query);
  const contratante = (parties.find((x) => x.rol === 'CONTRATANTE') || {}).persona || null;
  const c = AdmixData.carrierById(p.carrier);
  const coberturas = coberturasDe(p);

  return (
    <div style={{ flex: 1, overflow: 'auto' }}>
      <AxPageHeader
        breadcrumb={
          <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
            {origen ? (
              <button onClick={() => navigate(origen.path)} style={{
                appearance: 'none', cursor: 'default',
                background: 'var(--ax-bg-3)', border: '1px solid var(--ax-border-strong)',
                color: 'var(--ax-fg)', borderRadius: 14, height: 26, padding: '0 12px',
                fontSize: 12, display: 'inline-flex', alignItems: 'center', gap: 6,
              }}
                onMouseEnter={(e) => e.currentTarget.style.background = 'var(--ax-bg-4)'}
                onMouseLeave={(e) => e.currentTarget.style.background = 'var(--ax-bg-3)'}
              >
                <AxIcon name="arrow-left" size={12}/> Volver a {origen.label}
              </button>
            ) : null}
            <AxCrumb items={[{ label: 'Pólizas', to: '/polizas' }, { label: p.polizaNumero }]}/>
          </div>
        }
        eyebrow={(c ? c.name : '') + ' · ' + AdmixData.formatRamoLabel(p.ramo)}
        title={p.producto}
        subtitle={
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
            <span className="ax-mono" style={{ color: 'var(--ax-fg)' }}>{p.polizaNumero}</span>
            <AxStatusPill code={p.status}/>
            <span style={{ fontSize: 12, color: 'var(--ax-fg-muted)' }}>
              {AdmixData.formatMoney(p.primaTotal)} al año · {p.moneda} · {p.formaPago}
            </span>
          </span>
        }
        actions={
          <>
            {p.status === 'RENEWAL_PENDING' ? <AxButton variant="primary" icon="refresh">Preparar renovación</AxButton> : null}
            <AxButton variant="secondary" icon="edit">Editar</AxButton>
            <AxButton variant="secondary" icon="dots"/>
          </>
        }
      />

      <div style={{ padding: '20px 32px 48px', maxWidth: 1240 }}>
        {/* LAS TRES BARRAS — el orden en que el agente pregunta.
            Ancho completo a propósito: en móvil se apilan igual. */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginBottom: 18 }}>
          <BarraCobranza p={p} abierta={abierta === 'cobranza'} onToggle={() => toggle('cobranza')}/>
          <BarraContratante p={p} contratante={contratante} abierta={abierta === 'contratante'} onToggle={() => toggle('contratante')}/>
          <BarraPersonas
            p={p} titulo="Asegurados" gente={porRol.asegurados}
            vacio="Esta póliza no tiene asegurados ligados todavía."
            abierta={abierta === 'asegurados'} onToggle={() => toggle('asegurados')}
          />
          {porRol.beneficiarios.length > 0 ? (
            <BarraPersonas
              p={p} titulo="Beneficiarios" gente={porRol.beneficiarios} mostrarPorcentaje
              vacio="Sin beneficiarios designados."
              abierta={abierta === 'beneficiarios'} onToggle={() => toggle('beneficiarios')}
            />
          ) : null}
        </div>

        {/* LO DEMÁS — a un lado en escritorio, abajo en pantallas angostas. */}
        <div style={{ display: 'flex', gap: 18, alignItems: 'flex-start', flexWrap: 'wrap' }}>
          <div style={{ flex: '1 1 460px', minWidth: 0, display: 'flex', flexDirection: 'column', gap: 16 }}>
            <PolicyCard title="Coberturas" eyebrow={AdmixData.formatRamoLabel(p.ramo)}>
              <div style={{ overflowX: 'auto' }}>
                <table className="ax-table">
                  <thead>
                    <tr>
                      <th>Cobertura</th>
                      <th style={{ width: 150, textAlign: 'right' }}>Suma asegurada</th>
                      <th style={{ width: 110, textAlign: 'right' }}>Deducible</th>
                      <th style={{ width: 100, textAlign: 'right' }}>Coaseguro</th>
                    </tr>
                  </thead>
                  <tbody>
                    {coberturas.map((cob, i) => (
                      <tr key={i} style={{ cursor: 'default' }}>
                        <td>
                          <div style={{ fontSize: 13, color: 'var(--ax-fg-strong)' }}>{cob.nombre}</div>
                          <div style={{ fontSize: 10, color: 'var(--ax-fg-faint)' }}>{cob.tipo}</div>
                        </td>
                        <td style={{ textAlign: 'right' }} className="ax-mono">{cob.suma}</td>
                        <td style={{ textAlign: 'right' }} className="ax-mono">{cob.ded}</td>
                        <td style={{ textAlign: 'right' }} className="ax-mono">{cob.coa}</td>
                      </tr>
                    ))}
                  </tbody>
                </table>
              </div>
            </PolicyCard>

            <PolicyCard title="Financieros">
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: 12 }}>
                <Kv label="Prima neta" mono>{AdmixData.formatMoney(p.primaTotal * 0.86)}</Kv>
                <Kv label="Recargo por pago fraccionado" mono>{p.formaPago === 'Anual' ? '$ 0.00' : AdmixData.formatMoney(p.primaTotal * 0.04)}</Kv>
                <Kv label="IVA (16 %)" mono>{AdmixData.formatMoney(p.primaTotal * 0.16)}</Kv>
                <Kv label="Prima total" mono strong>{AdmixData.formatMoney(p.primaTotal)}</Kv>
              </div>
            </PolicyCard>
          </div>

          <div style={{ flex: '1 1 300px', minWidth: 0, display: 'flex', flexDirection: 'column', gap: 10 }}>
            <PolicyCardPlegable
              title="Datos de la póliza"
              resumen={AdmixData.formatDateES(p.inicioVigencia) + ' → ' + AdmixData.formatDateES(p.finVigencia)}
              abierta={datosAbiertos === 'datos'}
              onToggle={() => toggleDatos('datos')}
            >
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(130px, 1fr))', gap: 12 }}>
                <Kv label="Inicio de vigencia" mono>{AdmixData.formatDateES(p.inicioVigencia)}</Kv>
                <Kv label="Fin de vigencia" mono>{AdmixData.formatDateES(p.finVigencia)}</Kv>
                <Kv label="Moneda">{p.moneda}</Kv>
                <Kv label="Forma de pago">{p.formaPago}{p.formaPago !== 'Anual' ? ' · día ' + p.diaPago : ''}</Kv>
                <Kv label="Aseguradora">{c ? c.name : '—'}</Kv>
                <Kv label="Ramo">{AdmixData.formatRamoLabel(p.ramo)}</Kv>
              </div>
            </PolicyCardPlegable>

            <PolicyCardPlegable
              title="Línea de tiempo"
              resumen={'Leída ' + AdmixData.formatDateES(p.createdAt)}
              abierta={datosAbiertos === 'tiempo'}
              onToggle={() => toggleDatos('tiempo')}
            >
              <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
                {[
                  { date: p.createdAt, label: 'Leída del PDF', body: <span className="ax-mono" style={{ fontSize: 11 }}>{p.sourcePdf}</span>, kind: 'extract' },
                  { date: p.inicioVigencia, label: p.status === 'AUTO_APPROVED' ? 'Aprobada automáticamente' : 'Aprobada', body: p.status === 'AUTO_APPROVED' ? 'Coincidió con el catálogo' : 'Confirmada por ti', kind: 'approve' },
                  ...(p.status === 'RENEWAL_PENDING' ? [{ date: p.finVigencia, label: 'Renovación próxima', body: 'Vence ' + AdmixData.formatDateES(p.finVigencia), kind: 'renewal' }] : []),
                  ...(p.status === 'RENEWED' ? [{ date: p.inicioVigencia, label: 'Renovada', body: 'Vigencia extendida 12 meses', kind: 'renewal' }] : []),
                ].map((entry, i, arr) => (
                  <div key={i} style={{ display: 'flex', gap: 12 }}>
                    <div style={{ width: 22, display: 'flex', flexDirection: 'column', alignItems: 'center', flexShrink: 0 }}>
                      <span style={{
                        width: 8, height: 8, borderRadius: '50%',
                        background: entry.kind === 'approve' ? 'var(--ax-accent)' : entry.kind === 'renewal' ? 'var(--ax-info)' : 'var(--ax-fg-muted)',
                        border: '2px solid var(--ax-bg-2)', boxShadow: '0 0 0 1px var(--ax-border-strong)',
                      }}/>
                      {i < arr.length - 1 ? <span style={{ flex: 1, width: 1, background: 'var(--ax-border)', marginTop: 2 }}/> : null}
                    </div>
                    <div style={{ flex: 1, paddingBottom: i < arr.length - 1 ? 8 : 0 }}>
                      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
                        <span style={{ fontSize: 12, color: 'var(--ax-fg-strong)', fontWeight: 500 }}>{entry.label}</span>
                        <span style={{ fontSize: 10, color: 'var(--ax-fg-faint)', fontFamily: 'var(--font-mono)' }}>{AdmixData.formatDateES(entry.date)}</span>
                      </div>
                      <div style={{ fontSize: 12, color: 'var(--ax-fg-muted)', marginTop: 2 }}>{entry.body}</div>
                    </div>
                  </div>
                ))}
              </div>
            </PolicyCardPlegable>

            <PolicyCardPlegable
              title="Documentos de la póliza"
              resumen="1 archivo"
              abierta={datosAbiertos === 'docs'}
              onToggle={() => toggleDatos('docs')}
            >
              <button style={{
                appearance: 'none', textAlign: 'left', width: '100%',
                background: 'transparent', border: '1px solid var(--ax-border)',
                padding: '10px 12px', borderRadius: 6, color: 'inherit',
                display: 'flex', alignItems: 'center', gap: 10, cursor: 'default',
              }}
                onMouseEnter={(e) => e.currentTarget.style.background = 'var(--ax-bg-3)'}
                onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
              >
                <AxIcon name="pdf" size={16} style={{ color: 'var(--ax-fg-muted)' }}/>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div className="ax-mono" style={{ fontSize: 12, color: 'var(--ax-fg)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{p.sourcePdf}</div>
                  <div style={{ fontSize: 11, color: 'var(--ax-fg-muted)' }}>Emisión original · 8 pp</div>
                </div>
                <AxIcon name="eye" size={13} style={{ color: 'var(--ax-fg-faint)' }}/>
              </button>
            </PolicyCardPlegable>

            {contratante ? <OtrasPolizasCard p={p} contratante={contratante}/> : null}
          </div>
        </div>
      </div>
    </div>
  );
}

function OtrasPolizasCard({ p, contratante }) {
  const { navigate } = useHashRoute();
  const otras = AdmixPersonas.policiesOfPersona(contratante.id)
    .filter((r) => r.policy.id !== p.id && r.obligadoPago)
    .slice(0, 5);
  if (otras.length === 0) return null;
  return (
    <PolicyCard title={'Otras pólizas de ' + contratante.nombre.split(/\s+/)[0]} compact>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
        {otras.map((r) => (
          <button key={r.policy.id}
            onClick={() => navigate(AdmixPersonas.conOrigen('/polizas/' + r.policy.id, '/polizas/' + p.id, p.producto))}
            style={{
              appearance: 'none', background: 'transparent', border: 0,
              padding: '6px 4px', display: 'flex', alignItems: 'center', gap: 10,
              textAlign: 'left', color: 'inherit', cursor: 'default', borderRadius: 6,
            }}
            onMouseEnter={(e) => e.currentTarget.style.background = 'var(--ax-bg-3)'}
            onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
          >
            <AxCarrierAvatar id={r.policy.carrier} size={18}/>
            <span style={{ flex: 1, fontSize: 12, color: 'var(--ax-fg)' }}>{r.policy.producto}</span>
            <AxRamoTag code={r.policy.ramo}/>
            <span className="ax-mono" style={{ fontSize: 11, color: 'var(--ax-fg-muted)' }}>
              {AdmixData.formatMoney(r.policy.primaTotal)}
            </span>
          </button>
        ))}
      </div>
    </PolicyCard>
  );
}

function Kv({ label, children, mono, span2, strong }) {
  return (
    <div style={{ gridColumn: span2 ? '1 / -1' : undefined, minWidth: 0 }}>
      <div className="ax-eyebrow" style={{ marginBottom: 4, fontSize: 9 }}>{label}</div>
      <div style={{
        fontSize: strong ? 15 : 13, fontWeight: strong ? 700 : 400,
        color: 'var(--ax-fg-strong)', fontFamily: mono ? 'var(--font-mono)' : 'var(--font-body)',
        overflowWrap: 'anywhere',
      }}>
        {children}
      </div>
    </div>
  );
}

function FilterChip2({ label, value, onChange, options }) {
  return (
    <div style={{ position: 'relative' }}>
      <select value={value} onChange={(e) => onChange(e.target.value)} style={{
        appearance: 'none', background: 'var(--ax-bg-4)',
        border: '1px solid var(--ax-border)', color: 'var(--ax-fg)',
        padding: '0 28px 0 10px', height: 32, borderRadius: 6, fontSize: 12, cursor: 'default',
      }}>
        {options.map((o) => <option key={o.v} value={o.v}>{label}: {o.label}</option>)}
      </select>
      <AxIcon name="chevron-down" size={11} style={{
        position: 'absolute', right: 9, top: '50%', transform: 'translateY(-50%)',
        color: 'var(--ax-fg-faint)', pointerEvents: 'none',
      }}/>
    </div>
  );
}

Object.assign(window, { PoliciesScreen, FilterChip2, BarraCobranza, BarraFicha });
