// ─── Expedientes · Lista ────────────────────────────────────────────
// ExpedientesListScreen MIGRADA a ui/entity-list.jsx (EntityListScreen,
// prop `collection='expedientes'` — P6, ola 2): mueren ~250 líneas propias de
// paginación server-side/KPI-fallback/export/tabla que ya vivían, idénticas,
// en el motor genérico (el propio motor documenta que su patrón salió DE
// ACÁ). Se conservan: las columnas/render específicos de Expediente, los
// chips de pipeline (filterChips), vendedoresDisponibles()/
// vendedorIdPorNombre() (fuente única del equipo comercial + resolución de
// uuid real) y el toast + resaltado de fila del expediente recién creado
// (`micos-just-created`, sin equivalente genérico en el motor — se agregó un
// prop opcional `highlightRowId` a EntityListScreen para esto, ver su
// header). Expone a window: ExpedientesListScreen, vendedoresDisponibles,
// vendedorIdPorNombre. Consume window.EXPEDIENTES, window.MicosStore
// (getStats/fetchPage), window.EntityListScreen (ui/entity-list.jsx),
// STATUS_CONFIG/PIPELINE_STATUSES (dominio/pipeline.js), formatMoney/
// formatMoneyShort/formatDate (dominio/formato.js).
//
// EXCEPCIÓN DE TECHO (regla ~250/400 duro — FIX P13 ítem 5: los conteos en
// comentarios siempre vencen, se borra el número puntual en vez de mantenerlo
// desactualizado; ver `wc -l` real para el valor vigente): un solo tema (la
// lista de Expedientes) con 7 columnas de render custom + el fallback de
// KPIs srv/memoria + el toast de "recién creado" — partirlo más metería
// helpers `Helper1`/`Helper2` sin nombre propio real, contra la propia regla
// de excepciones (partir por TEMA, no por tamaño arbitrario).

const { useState: _useStateL, useMemo: _useMemoL, useEffect: _useEffectL } = React;

// ─── Vendedores (fuente única del equipo comercial) ───────────────

// Vendedores tomados de los datos existentes (sin duplicar)
function vendedoresDisponibles() {
  // Fuente única: usuarios del equipo (todos menos el ADMIN), mostrados "Nombre Apellido".
  // Con el modelo de roles ADMIN/JEFE/USUARIO cualquiera del equipo puede ser vendedor de
  // un expediente (antes filtraba rol VENDEDOR, que ya no existe). A afinar si se restringe.
  const fromUsers = (window.USUARIOS || [])
    .filter((u) => u.rol !== 'ADMIN' && u.activo !== false)
    .map((u) => `${u.nombre} ${u.apellido}`);
  if (fromUsers.length) return fromUsers;
  // Fallback: derivar de los expedientes existentes.
  return Array.from(new Set(EXPEDIENTES.map((e) => e.vendedor?.name).filter(Boolean)));
}

// Resuelve el uuid real (`usuarios.id`) del vendedor elegido en el <select>, que solo
// maneja el nombre mostrado ("Nombre Apellido" — ver vendedoresDisponibles arriba). Bug
// demo-crítico (BUGS-caza-01-07-2026.md #1): el alta mandaba `vendedor: {name}` sin id; el
// backend (whitelist EXP_COLUMNS en serializers/expedientes.ts) descarta ese objeto y
// espera `vendedor_id` (NOT NULL) -> 400 SIEMPRE. Si el nombre no cambió respecto del
// vendedor ya asignado (edición) preferimos el id que ya vino del backend (embed
// `vendedor:usuarios!...(*)`, ver vendedorObj) — así no se rompe si ese usuario ya no
// figura en USUARIOS activos. Si cambió (o es alta nueva), matcheamos por nombre exacto
// contra window.USUARIOS (hidratado con ids reales de la DB).
function vendedorIdPorNombre(nombre, vendedorOriginal) {
  if (!nombre) return null;
  if (vendedorOriginal && vendedorOriginal.name === nombre && vendedorOriginal.id) {
    return vendedorOriginal.id;
  }
  const match = (window.USUARIOS || []).find((u) => `${u.nombre} ${u.apellido}` === nombre);
  return match ? match.id : null;
}

// NOTA (BUGS-caza-01-07-2026.md #25/B1): antes había acá un `nextExpedientePublicId(rows)`
// que fabricaba el próximo EXP-XXXXXXXX sumando 1 al máximo de `rows` (la ventana
// hidratada en memoria, capada a BIG_LIMIT=500 de 2103 reales) — con la DB completa podía
// devolver un publicId que YA EXISTE en un expediente REAL fuera de esa ventana. Verificado
// que el backend no llega a insertarlo duplicado (`snakeizeRow` en rest.ts SIEMPRE borra
// `public_id` del body antes del insert/update, sin importar lo que mande el cliente — el
// trigger `trg_assign_public_id`/secuencia `seq_exp` es la única fuente real), pero el
// FRONT sí mostraba ese id fabricado como si fuera el real. Se eliminó: ya no se fabrica
// acá, el header/toast muestran un placeholder honesto mientras el POST está en vuelo (ver
// screens/expedientes/alta.jsx `handleSave` / detalle.jsx).

// Paciente/Médico pueden venir como {} (sin asignar) en expedientes reales — la API
// devuelve {} en vez de null para no romper (ver /api/expedientes). Estos helpers dan el
// label «Nombre Apellido» (médico con título) o '' si no hay; el render cae al guion '—'.
const _expPacNom = (e) => (e.particular && e.particular.nombre)
  ? `${e.particular.nombre} ${e.particular.apellido || ''}`.trim() : '';
const _expMedNom = (e) => (e.medico && e.medico.nombre)
  ? `${e.medico.titulo ? e.medico.titulo + ' ' : ''}${e.medico.nombre} ${e.medico.apellido || ''}`.trim() : '';

// Column widths — minimum floor por columna, excedente repartido igual (mismo criterio
// que entity-list.jsx: string CSS grid-template válida, no solo 'Npx').
const _EXP_COLS_WIDTH = {
  publicId: 'minmax(150px,1fr)', paciente: 'minmax(190px,1fr)', medico: 'minmax(160px,1fr)',
  entidadVendedor: 'minmax(170px,1fr)', ganancia: 'minmax(120px,1fr)', total: 'minmax(130px,1fr)',
};

// ─── Expedientes List Screen ──────────────────────
function ExpedientesListScreen({ onOpen, onNew, prefilter }) {
  const [justCreatedId, setJustCreatedId] = _useStateL(null);
  const [toast, setToast] = _useStateL(null);

  // Al volver desde la pantalla de alta: resaltar el expediente recién creado.
  _useEffectL(() => {
    let id = null;
    try { id = localStorage.getItem('micos-just-created'); } catch (_) {}
    if (!id) return;
    try { localStorage.removeItem('micos-just-created'); } catch (_) {}
    const exp = EXPEDIENTES.find((e) => e.id === id);
    if (!exp) return;
    setJustCreatedId(id);
    // El publicId lo asigna el trigger de la DB (BUGS-caza-01-07-2026.md #25/B1), no el
    // cliente — placeholder honesto por si este toast dispara antes de que el POST
    // resuelva (ventana muy breve; solo si el usuario vuelve a la lista casi al instante).
    setToast(`${exp.publicId || 'Expediente'} creado · ${_expPacNom(exp) || 'Sin paciente'}`);
    const t1 = setTimeout(() => setJustCreatedId(null), (window.TOAST_MS || {}).corto || 2800);
    const t2 = setTimeout(() => setToast(null), (window.TOAST_MS || {}).largo || 4200);
    return () => { clearTimeout(t1); clearTimeout(t2); };
  }, []);

  // Todas las filas del universo (para el fallback de KPIs cuando hay prefilter — ver
  // abajo — y como fallback de EntityListScreen si el fetchPage inicial falla).
  const allRows = _useMemoL(() => [...EXPEDIENTES], []);
  const baseRows = _useMemoL(() => {
    if (prefilter?.predicate) return allRows.filter(prefilter.predicate);
    return allRows;
  }, [prefilter, allRows]);

  // Fallback en memoria (reduce/filter sobre el array hidratado — hoy capado a
  // BIG_LIMIT=500, así que con más filas que eso cuenta MAL). Se usa mientras cargan los
  // stats del servidor, si el fetch falla, o si hay un prefilter activo (v_expedientes_stats
  // agrega la colección ENTERA, no sabe nada del prefilter cross-módulo).
  const stats = _useMemoL(() => {
    const total = baseRows.length;
    const active = baseRows.filter((e) => e.status !== 'RECIBO').length;
    const totalFact = baseRows.reduce((s, e) => s + (e.total || 0), 0);
    const withPct = baseRows.filter((e) => e.gananciaPct != null);
    const avgPct = withPct.length ? withPct.reduce((s, e) => s + e.gananciaPct, 0) / withPct.length : null;
    const counts = {};
    baseRows.forEach((e) => counts[e.status] = (counts[e.status] || 0) + 1);
    return { total, active, totalFact, avgPct, counts };
  }, [baseRows]);

  // KPIs server-side: GET /api/expedientes/stats agrega en Postgres sobre TODO el
  // universo (no el array capado). Se pide una sola vez al montar; mientras no llegó (o
  // si falla) `srvStats` queda null y se usa el fallback en memoria de arriba.
  const [srvStats, setSrvStats] = _useStateL(null);
  _useEffectL(() => {
    let cancelled = false;
    window.MicosStore.getStats('expedientes')
      .then((data) => { if (!cancelled) setSrvStats(data); })
      .catch((e) => { console.warn('[ExpedientesListScreen] getStats', e.message); });
    return () => { cancelled = true; };
  }, []);

  const kpi = (!prefilter && srvStats) ? {
    total: srvStats.total ?? stats.total,
    active: srvStats.activos ?? stats.active,
    totalFact: srvStats.sumTotal ?? stats.totalFact,
    avgPct: srvStats.avgGananciaPct != null ? Number(srvStats.avgGananciaPct) : stats.avgPct,
    counts: srvStats.byEstado || stats.counts,
  } : stats;

  const expExportConfig = [
    { label: 'Expediente', value: (e) => e.publicId },
    { label: 'Fecha inicio', value: (e) => e.inicio },
    { label: 'Paciente', value: (e) => _expPacNom(e) || '—' },
    { label: 'Médico', value: (e) => _expMedNom(e) || '—' },
    { label: 'Entidad', value: (e) => (e.entidad ? e.entidad.razonSocial : '—') },
    { label: 'Vendedor', value: (e) => e.vendedor.name || 'Sin vendedor' },
    // Honesto: sin costo real en la DB, ganancia/gananciaPct son null para todo expediente
    // migrado (ver public/erp/CLAUDE.md) — exportar 0 se leería como "ganancia real cero",
    // no como "sin dato". '—' distingue ambos casos en el Excel.
    { label: 'Ganancia', value: (e) => (e.ganancia != null ? e.ganancia : '—') },
    { label: '% Ganancia', value: (e) => (e.gananciaPct != null ? e.gananciaPct + '%' : '—') },
    { label: 'Total', value: (e) => e.total || 0 },
    { label: 'Estado', value: (e) => (STATUS_CONFIG[e.status] || {}).label || e.status },
  ];

  return (
    <EntityListScreen
      eyebrow="Operaciones · Expedientes"
      title="Expedientes"
      description="Pipeline de ventas, presupuestos y facturación."
      primaryAction={{ label: 'Nuevo expediente', icon: IconPlus, onClick: onNew }}
      secondaryAction={{ label: 'Exportar', icon: IconDownload }}
      kpis={[
        // Prop `delta` MUERTA en KpiCard (P4) — no se pasa más (regla del manifest: los
        // callers de KpiCard limpian delta/highlighted al partir su archivo de pantalla).
        // FIX hallazgo auditoría 05/07/2026 (BAJA): los `trend` de las 4 KPI tenían 6 puntos
        // literales hardcodeados + solo el último real — no hay serie histórica real detrás
        // (CLAUDE.md decisión estructural #3: "Nada de... KPIs en memoria" mostrados como
        // reales). Sin `trend`, KpiCard (ui/primitives.jsx) no renderiza el sparkline —
        // degrada a solo label+value, sin inventar una curva.
        { label: 'Total', value: String(kpi.total), deltaTone: 'neutral', icon: IconFileText },
        { label: 'En proceso', value: String(kpi.active), deltaTone: 'neutral', icon: IconClock },
        { label: 'Monto en curso', value: formatMoneyShort(kpi.totalFact), deltaTone: 'neutral', icon: IconDollar },
        { label: 'Ganancia promedio', value: kpi.avgPct != null ? `${kpi.avgPct.toFixed(1)}%` : 'N/D', deltaTone: 'neutral', icon: IconTrendingUp },
      ]}
      filterChips={{
        allLabel: 'Todos',
        options: PIPELINE_STATUSES
          .map((s) => ({ label: STATUS_CONFIG[s].label, value: s, count: kpi.counts[s] || 0, pill: STATUS_CONFIG[s].pill }))
          .filter((o) => o.count > 0),
        applyFilter: (e, v) => e.status === v,
      }}
      columns={[
        { key: 'publicId', label: 'Expediente', width: _EXP_COLS_WIDTH.publicId, render: (e) => <IdDateCell id={e.publicId} date={e.inicio} /> },
        {
          key: 'paciente', label: 'Paciente', width: _EXP_COLS_WIDTH.paciente, render: (e) => (
            _expPacNom(e) ? (
              <div className="flex items-center gap-3 min-w-0">
                <Avatar name={_expPacNom(e)} />
                <p className="text-[13px] font-bold truncate" style={{ color: 'var(--foreground)' }}>{_expPacNom(e)}</p>
              </div>
            ) : <p className="text-[13px] font-medium italic truncate" style={{ color: 'color-mix(in oklab, var(--muted-foreground) 80%, transparent)' }}>—</p>
          ),
        },
        {
          key: 'medico', label: 'Médico', width: _EXP_COLS_WIDTH.medico, render: (e) => (
            _expMedNom(e)
              ? <p className="text-[13px] font-medium truncate" style={{ color: 'color-mix(in oklab, var(--foreground) 85%, transparent)' }}>{_expMedNom(e)}</p>
              : <p className="text-[13px] font-medium italic truncate" style={{ color: 'color-mix(in oklab, var(--muted-foreground) 80%, transparent)' }}>—</p>
          ),
        },
        {
          key: 'entidadVendedor', label: 'Entidad / Vendedor', width: _EXP_COLS_WIDTH.entidadVendedor, render: (e) => (
            <div className="min-w-0">
              {e.entidad
                ? <p className="text-[13px] font-semibold truncate" style={{ color: 'var(--foreground)' }}>{e.entidad.razonSocial}</p>
                : <p className="text-[13px] font-medium italic truncate" style={{ color: 'color-mix(in oklab, var(--muted-foreground) 80%, transparent)' }}>—</p>}
              <p className="text-[11.5px] font-medium truncate mt-0.5" style={{ color: 'var(--muted-foreground)' }}>{e.vendedor.name || 'Sin vendedor'}</p>
            </div>
          ),
        },
        {
          key: 'ganancia', label: 'Ganancia', width: _EXP_COLS_WIDTH.ganancia, render: (e) => (
            e.ganancia ? (
              <>
                <p className="text-[13px] font-semibold tabular-nums" style={{ color: 'var(--foreground)' }}>{formatMoney(e.ganancia)}</p>
                {e.gananciaPct != null && (
                  <p className="text-[12px] font-bold tabular-nums mt-0.5" style={{ color: e.gananciaPct >= 35 ? 'var(--success)' : e.gananciaPct >= 25 ? 'var(--warning)' : 'var(--muted-foreground)' }}>
                    {e.gananciaPct}%
                  </p>
                )}
              </>
            ) : <span style={{ color: 'color-mix(in oklab, var(--muted-foreground) 60%, transparent)' }}>—</span>
          ),
        },
        {
          key: 'total', label: 'Total', width: _EXP_COLS_WIDTH.total, render: (e) => (
            e.total
              ? <p className="text-[14px] font-extrabold tabular-nums" style={{ color: 'var(--foreground)' }}>{formatMoney(e.total)}</p>
              : <span style={{ color: 'color-mix(in oklab, var(--muted-foreground) 60%, transparent)' }}>—</span>
          ),
        },
        { key: 'estado', label: 'Estado', width: 'minmax(140px,1fr)', render: (e) => <StatusPill status={e.status} size="sm" /> },
      ]}
      rows={allRows}
      collection="expedientes"
      searchPlaceholder="Buscar por ID, paciente, médico o entidad…"
      searchFilter={(e, q) =>
        e.publicId.toLowerCase().includes(q) ||
        _expPacNom(e).toLowerCase().includes(q) ||
        _expMedNom(e).toLowerCase().includes(q) ||
        (e.entidad?.razonSocial || '').toLowerCase().includes(q) ||
        (e.vendedor.name || '').toLowerCase().includes(q)
      }
      onRowClick={onOpen}
      rowChevron
      dateField="inicio"
      // Orden por defecto: más nuevo → más viejo (por fecha de inicio, desempata por
      // publicId). Desde P15 el server YA ordena igual (.order('inicio', desc) —
      // antes ordenaba por created_at, que el bulk de Contabilium comparte entero
      // en un único valor de carga), así que esto ya no compensa un defecto del
      // server: es la red de seguridad de siempre para altas optimistas (fila
      // nueva insertada en memoria antes de que hydrate() la vuelva a traer
      // ordenada) y para cuando `collection` no pagina server-side.
      sortRows={(a, b) =>
        (b.inicio || '').localeCompare(a.inicio || '') ||
        (b.publicId || '').localeCompare(a.publicId || '')
      }
      highlightRowId={justCreatedId}
      prefilter={prefilter}
      exportFilename="Expedientes"
      exportConfig={expExportConfig}
      extraBelow={toast && (
        <div
          className="fixed bottom-6 right-6 z-50 flex items-center gap-3 rounded-xl px-4 py-3 animate-fade-in-up"
          style={{
            background: 'var(--card)',
            border: '1px solid color-mix(in oklab, var(--accent) 45%, transparent)',
            boxShadow: '0 18px 50px -14px rgba(8, 20, 24, 0.5)',
            maxWidth: 'min(92vw, 380px)',
          }}
        >
          <span className="inline-flex items-center justify-center rounded-lg flex-shrink-0"
            style={{ width: 30, height: 30, background: 'color-mix(in oklab, var(--accent) 16%, transparent)', color: 'var(--accent)' }}>
            <IconCheck size={16} />
          </span>
          <p className="text-[12.5px] font-bold truncate" style={{ color: 'var(--foreground)' }}>{toast}</p>
        </div>
      )}
    />
  );
}

Object.assign(window, { ExpedientesListScreen, vendedoresDisponibles, vendedorIdPorNombre });
