// ─── Micos CRM · Envío: borradores + plantillas + adjuntos salientes ────────
// Maquinaria compartida entre el composer (crm/composer.jsx) y la respuesta
// inline del hilo (crm/thread.jsx): autosave real de borradores
// (/api/crm-borradores, contrato B0-migrador), plantillas con variables
// REEMPLAZADAS (nunca viaja un {{...}} crudo — auditoría #33), adjuntos
// salientes (archivo local + comprobante del ERP rendereado a PDF) y el
// cálculo de destinatarios de responder-a-todos/reenviar (auditoría #17/#18).
// Expone a window: crmDetalleExpediente, crmPrepararPlantilla,
// CrmPlantillasDropdown, useCrmBorrador, useCrmAdjuntosSalientes,
// CrmChipsAdjuntos, CrmPickerComprobantes, crmPayloadAdjuntos,
// crmDestinatariosRespuesta, crmCitaReenvio, crmParseEmails.
// Consume: /api/expedientes/detail (docs reales del expediente), los renderers
// de print/ (MicosDocPresupuesto/FacturaA-B/Remito/Recibo + MicosDocCommon +
// jspdf, cargados por Micos CRM.html), CRM_TEMPLATES/CRM_ALIASES (crm/data.js),
// formatMoney/formatDate (dominio/formato.js), íconos (ui/icons.jsx).

const { useState: _useStateEV, useEffect: _useEffectEV, useRef: _useRefEV } = React;

// ── Detalle rico del expediente (cacheado) ─────────────────────────────────
// /api/expedientes/detail junta los documentos REALES por FK en Postgres.
// Cache por referencia (publicId EXP-… o uuid) — lo comparten timeline,
// plantillas y picker de comprobantes, así el panel no re-pide en cada render.
const _crmDetalleCache = new Map();
function crmDetalleExpediente(ref) {
  if (!ref) return Promise.resolve(null);
  if (_crmDetalleCache.has(ref)) return _crmDetalleCache.get(ref);
  const esPublicId = /^l?-?[A-Za-z]{2,}-/i.test(ref) || String(ref).toUpperCase().startsWith('EXP-');
  const qs = esPublicId ? `publicId=${encodeURIComponent(ref)}` : `id=${encodeURIComponent(ref)}`;
  const p = fetch(`/api/expedientes/detail?${qs}`, { credentials: 'same-origin' })
    .then((r) => (r.ok ? r.json() : null))
    .then((exp) => {
      if (!exp || !exp.id) return null;
      // Mergear en las colecciones (mismo patrón que detalle.jsx del ERP):
      // así los renderers/lookups ven los docs aunque estén fuera de ventana.
      if (window.MicosMergeDetalleRico) window.MicosMergeDetalleRico(exp);
      return exp;
    })
    .catch(() => null);
  _crmDetalleCache.set(ref, p);
  // Un fetch fallido no queda cacheado como null para siempre.
  p.then((v) => { if (v === null) _crmDetalleCache.delete(ref); });
  return p;
}
// El vínculo puede cambiar (asociar/crear expediente) — invalidar puntual.
function crmInvalidarDetalle(ref) { if (ref) _crmDetalleCache.delete(ref); }

// ── Plantillas con variables reales ────────────────────────────────────────
function _crmCodigoFactura(f) {
  const pv = String(f.puntoVenta == null ? '' : f.puntoVenta).padStart(4, '0');
  return [f.tipoFactura, pv, f.numero].filter(Boolean).join('-');
}
// Arma el diccionario de variables con lo que REALMENTE sabemos del hilo/
// expediente. Lo que no se puede resolver queda ausente y crmAplicarVariables
// lo baja a "[completar]" visible — jamás viaja un {{...}} crudo (#33).
function _crmContextoPlantilla({ email, exp, user }) {
  const ctx = {};
  const money = window.formatMoney || ((n) => `$${n}`);
  const fecha = window.formatDate || ((d) => d);
  if (user) ctx.usuario = `${user.nombre || ''} ${user.apellido || ''}`.trim() || user.email || '';
  if (email) {
    ctx.asunto_original = email.asunto || '';
    const msgs = email.messages || [];
    const lastIn = [...msgs].reverse().find((m) => m.direccion === 'entrada');
    if (lastIn && lastIn.from && lastIn.from.nombre) ctx.contacto_nombre = lastIn.from.nombre;
  }
  if (exp) {
    ctx.exp_id = exp.publicId || '';
    if (exp.medico && (exp.medico.nombre || exp.medico.apellido) && exp.medico.apellido !== 'Sin asignar') {
      ctx.medico_nombre = `${exp.medico.titulo ? exp.medico.titulo + ' ' : ''}${exp.medico.nombre || ''} ${exp.medico.apellido || ''}`.trim();
    }
    const pres = exp.presupuesto;
    if (pres) {
      const total = pres.total != null ? pres.total : exp.total;
      if (total != null) ctx.presupuesto_monto = money(total);
      if (Array.isArray(pres.items) && pres.items.length) {
        ctx.lista_materiales = pres.items
          .map((it) => `${it.descripcion || it.sku || 'Ítem'} × ${it.cantidad != null ? it.cantidad : 1}`)
          .join('\n· ');
      }
    } else if (exp.total != null) {
      ctx.presupuesto_monto = money(exp.total);
    }
    const fac = Array.isArray(exp.facturas) && exp.facturas.length ? exp.facturas[exp.facturas.length - 1] : null;
    if (fac) {
      ctx.factura_id = _crmCodigoFactura(fac);
      if (fac.total != null) ctx.factura_monto = money(fac.total);
      if (fac.fechaEmision) ctx.factura_fecha = fecha(fac.fechaEmision);
      if (fac.fechaVencPago) ctx.factura_venc = fecha(fac.fechaVencPago);
    }
  }
  // cirugia_fecha / lista_materiales sin presupuesto: no hay dato estructurado
  // en el ERP (la fecha de cirugía es texto libre) → caen a [completar].
  return ctx;
}
function crmAplicarVariables(texto, ctx) {
  return String(texto || '').replace(/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g, (m, k) => {
    const v = ctx[k];
    return v != null && String(v).trim() !== '' ? String(v) : '[completar]';
  });
}
// Resuelve una plantilla completa (asunto + cuerpo) con las variables del
// hilo/expediente vinculado. Async porque puede necesitar el detalle rico.
function crmPrepararPlantilla(tpl, { email, user }) {
  const ref = email && email.linkedExpedienteId;
  const base = ref ? crmDetalleExpediente(ref) : Promise.resolve(null);
  return base.then((exp) => {
    const ctx = _crmContextoPlantilla({ email, exp, user });
    return { asunto: crmAplicarVariables(tpl.asunto, ctx), cuerpo: crmAplicarVariables(tpl.cuerpo, ctx) };
  });
}

// Dropdown de plantillas — mismo visual que tenía el composer, extraído para
// que la respuesta inline (#21) use EXACTAMENTE el mismo componente.
function CrmPlantillasDropdown({ open, onClose, onPick, arriba = true }) {
  if (!open) return null;
  return (
    <>
      <div className="fixed inset-0 z-30" onClick={onClose} />
      <div
        className={`absolute ${arriba ? 'bottom-full mb-2' : 'top-full mt-2'} left-0 z-40 rounded-xl overflow-hidden`}
        style={{ background: 'var(--card)', border: '1px solid var(--border)', boxShadow: 'var(--shadow-lg)', width: 320 }}
      >
        <div className="px-3 py-2.5" style={{ borderBottom: '1px solid var(--border)' }}>
          <p className="text-[10.5px] font-bold uppercase tracking-[0.12em]" style={{ color: 'var(--muted-foreground)' }}>
            Plantillas
          </p>
        </div>
        {(window.CRM_TEMPLATES || []).map((t) => (
          <button
            key={t.id}
            type="button"
            onClick={() => onPick(t)}
            className="w-full text-left px-3 py-2.5 cursor-pointer flex items-start gap-2.5"
            style={{ borderBottom: '1px solid color-mix(in oklab, var(--border) 50%, transparent)' }}
            onMouseEnter={(e) => e.currentTarget.style.background = 'var(--surface)'}
            onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
          >
            <div className="min-w-0 flex-1">
              <p className="text-[12.5px] font-extrabold" style={{ color: 'var(--foreground)' }}>{t.nombre}</p>
              <p className="text-[11px] mt-0.5 truncate" style={{ color: 'var(--muted-foreground)' }}>{t.asunto}</p>
            </div>
            <span className="font-mono text-[10px] font-bold px-1.5 py-0.5 rounded flex-shrink-0" style={{ background: 'var(--surface)', color: 'var(--accent)' }}>
              {t.shortcut}
            </span>
          </button>
        ))}
      </div>
    </>
  );
}

// ── Borradores: autosave real (/api/crm-borradores, debounce 2s) ───────────
function crmParseEmails(str) {
  return String(str || '').split(/[,;]/).map((s) => s.trim()).filter(Boolean);
}
function _crmHoraCorta(iso) {
  const d = iso ? new Date(iso) : new Date();
  return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
}
// datos = { para, cc, asunto, cuerpo, alias } (strings crudos de los inputs).
// Devuelve { estado, restaurar, descartar } — `estado`: null | 'guardando' |
// 'error' | 'HH:MM' (hora del último guardado). descartar() borra el borrador
// del server (se usa al enviar Y al descartar a mano — contrato B0).
function useCrmBorrador({ activo, hiloId, datos }) {
  const [estado, setEstado] = _useStateEV(null);
  const idRef = _useRefEV(null);
  const lastRef = _useRefEV(null);
  const timerRef = _useRefEV(null);
  const colaRef = _useRefEV(Promise.resolve());
  const datosRef = _useRefEV(datos);
  datosRef.current = datos;

  const serial = JSON.stringify(datos);
  const tieneContenido = !!((datos.cuerpo || '').trim() || (datos.asunto || '').trim() || (datos.para || '').trim() || (datos.cc || '').trim());

  const guardar = () => {
    const d = datosRef.current;
    const s = JSON.stringify(d);
    if (s === lastRef.current) return; // nada nuevo que guardar
    setEstado('guardando');
    colaRef.current = colaRef.current.then(async () => {
      try {
        const payload = {
          hiloId: hiloId || null,
          para: crmParseEmails(d.para), cc: crmParseEmails(d.cc),
          asunto: d.asunto || '', cuerpo: d.cuerpo || '', alias: d.alias || null,
        };
        const url = idRef.current ? `/api/crm-borradores/${idRef.current}` : '/api/crm-borradores';
        const r = await fetch(url, {
          method: idRef.current ? 'PATCH' : 'POST',
          credentials: 'same-origin',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(payload),
        });
        if (!r.ok) throw new Error('borrador ' + r.status);
        const row = await r.json();
        if (!idRef.current && row && row.id) idRef.current = row.id;
        lastRef.current = s;
        setEstado(_crmHoraCorta());
      } catch (e) {
        setEstado('error');
      }
    });
  };

  _useEffectEV(() => {
    if (!activo) return;
    if (serial === lastRef.current) return;
    if (!tieneContenido && !idRef.current) return;
    clearTimeout(timerRef.current);
    timerRef.current = setTimeout(guardar, 2000);
    return () => clearTimeout(timerRef.current);
  }, [activo, serial]);

  // Retomar el borrador más reciente (del hilo, o suelto si hiloId null).
  const restaurar = async () => {
    try {
      const url = hiloId ? `/api/crm-borradores?hiloId=${encodeURIComponent(hiloId)}` : '/api/crm-borradores';
      const r = await fetch(url, { credentials: 'same-origin' });
      if (!r.ok) return null;
      let rows = await r.json();
      if (!Array.isArray(rows)) return null;
      if (!hiloId) rows = rows.filter((b) => !b.hiloId);
      const b = rows[0];
      if (!b) return null;
      idRef.current = b.id;
      setEstado(_crmHoraCorta(b.updatedAt));
      return b;
    } catch (e) { return null; }
  };

  // Borra el borrador (enviar / descartar). Fire-and-forget: el contenido ya
  // viajó (o el usuario ya decidió tirarlo) — no bloqueamos la UI por esto.
  const descartar = () => {
    clearTimeout(timerRef.current);
    const id = idRef.current;
    idRef.current = null;
    lastRef.current = null;
    setEstado(null);
    if (id) fetch(`/api/crm-borradores/${id}`, { method: 'DELETE', credentials: 'same-origin' }).catch(() => {});
  };

  // Nueva sesión de escritura (reabrir composer / cambiar de hilo): suelta el
  // borrador anterior SIN borrarlo del server — cerrar sin enviar lo conserva.
  const reiniciar = () => {
    clearTimeout(timerRef.current);
    idRef.current = null;
    lastRef.current = null;
    setEstado(null);
  };

  // Guardado inmediato (al cerrar/minimizar): si había un debounce pendiente,
  // no se pierden las últimas teclas.
  const flush = () => {
    if (!timerRef.current) return;
    clearTimeout(timerRef.current);
    timerRef.current = null;
    const d = datosRef.current;
    const hay = !!((d.cuerpo || '').trim() || (d.asunto || '').trim() || (d.para || '').trim() || (d.cc || '').trim());
    if (hay || idRef.current) guardar();
  };

  return { estado, restaurar, descartar, reiniciar, flush };
}
// Línea de estado del borrador — reemplaza el "Borrador guardado · hace un
// instante" ESTÁTICO de la auditoría (#22/#31) por el estado real del autosave.
function CrmEstadoBorrador({ estado }) {
  if (!estado) return null;
  const texto = estado === 'guardando' ? 'Guardando borrador…'
    : estado === 'error' ? 'No se pudo guardar el borrador'
    : `Borrador guardado · ${estado}`;
  return (
    <span className="text-[10.5px]" style={{ color: estado === 'error' ? 'var(--destructive)' : 'var(--muted-foreground)' }}>
      {texto}
    </span>
  );
}

// ── Adjuntos salientes (archivo local + PDF del ERP + reenvío) ─────────────
const _CRM_ADJ_MAX_ARCHIVO = 10 * 1024 * 1024; // 10 MB por archivo
const _CRM_ADJ_MAX_TOTAL = 15 * 1024 * 1024;   // 15 MB por mensaje
function _crmTipoAdjunto(mime, nombre) {
  const m = String(mime || '').toLowerCase();
  const n = String(nombre || '').toLowerCase();
  if (m.includes('pdf') || n.endsWith('.pdf')) return 'pdf';
  if (m.startsWith('image/') || /\.(png|jpe?g|gif|webp)$/.test(n)) return 'img';
  if (m.includes('sheet') || m.includes('excel') || m.includes('csv') || /\.(xlsx?|csv)$/.test(n)) return 'xls';
  return 'doc';
}
// items: { nombre, tipo, sizeBytes, origen: 'local'|'erp'|'reenvio',
//          dataBase64? (local/erp) | adjuntoId? (reenvío de un adjunto real) }
function useCrmAdjuntosSalientes() {
  const [adjuntos, setAdjuntos] = _useStateEV([]);
  const [error, setError] = _useStateEV(null);

  const _totalBytes = (list) => list.reduce((acc, a) => acc + (a.sizeBytes || 0), 0);
  const _sumar = (items) => {
    setError(null);
    setAdjuntos((prev) => {
      const next = [...prev];
      for (const it of items) {
        if ((it.sizeBytes || 0) > _CRM_ADJ_MAX_ARCHIVO) {
          setError(`"${it.nombre}" pesa más de 10 MB — no se puede adjuntar.`);
          continue;
        }
        if (_totalBytes(next) + (it.sizeBytes || 0) > _CRM_ADJ_MAX_TOTAL) {
          setError('Los adjuntos superan los 15 MB en total — quitá alguno.');
          break;
        }
        next.push(it);
      }
      return next;
    });
  };

  const agregarArchivos = (fileList) => {
    const files = Array.from(fileList || []);
    files.forEach((f) => {
      const reader = new FileReader();
      reader.onload = () => {
        const dataBase64 = String(reader.result || '').split(',')[1] || '';
        _sumar([{ nombre: f.name, tipo: _crmTipoAdjunto(f.type, f.name), mime: f.type || 'application/octet-stream', sizeBytes: f.size, origen: 'local', dataBase64 }]);
      };
      reader.onerror = () => setError(`No se pudo leer "${f.name}".`);
      reader.readAsDataURL(f);
    });
  };
  const agregarItem = (item) => _sumar([item]);
  const quitar = (idx) => { setError(null); setAdjuntos((prev) => prev.filter((_, i) => i !== idx)); };
  const limpiar = () => { setError(null); setAdjuntos([]); };

  return { adjuntos, error, agregarArchivos, agregarItem, quitar, limpiar };
}
// Parte del body del POST que entienden /api/crm-hilos y /api/crm-mensajes:
// `adjuntos` = archivos nuevos (base64) · `adjuntosDeIds` = adjuntos reales
// existentes re-enviados (el server copia la fila compartiendo storage_path).
function crmPayloadAdjuntos(adjuntos) {
  const out = {};
  const nuevos = adjuntos.filter((a) => a.dataBase64);
  const reenvio = adjuntos.filter((a) => a.adjuntoId);
  if (nuevos.length) out.adjuntos = nuevos.map((a) => ({ nombre: a.nombre, mime: a.mime || 'application/octet-stream', dataBase64: a.dataBase64 }));
  if (reenvio.length) out.adjuntosDeIds = reenvio.map((a) => a.adjuntoId);
  return out;
}
function _crmPesoLegible(bytes) {
  if (bytes == null) return null;
  if (bytes < 1024) return `${bytes} B`;
  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
// Chips de los adjuntos que van a salir con el mensaje (con quitar).
function CrmChipsAdjuntos({ adjuntos, onQuitar, error }) {
  if (!adjuntos.length && !error) return null;
  return (
    <div className="px-4 pb-2">
      {adjuntos.length > 0 && (
        <div className="flex flex-wrap gap-1.5">
          {adjuntos.map((a, i) => (
            <span
              key={`${a.nombre}-${i}`}
              className="inline-flex items-center gap-1.5 rounded-full pl-2.5 pr-1 py-1 text-[11px] font-semibold max-w-[260px]"
              style={{ background: 'var(--surface)', border: '1px solid var(--border)', color: 'var(--foreground)' }}
              title={a.nombre}
            >
              <IconPaperclip size={11} style={{ color: 'var(--muted-foreground)', flexShrink: 0 }} />
              <span className="truncate">{a.nombre}</span>
              {_crmPesoLegible(a.sizeBytes) && (
                <span style={{ color: 'var(--muted-foreground)' }}>{_crmPesoLegible(a.sizeBytes)}</span>
              )}
              <button
                type="button"
                onClick={() => onQuitar(i)}
                className="inline-flex items-center justify-center rounded-full cursor-pointer flex-shrink-0"
                style={{ width: 18, height: 18, color: 'var(--muted-foreground)' }}
                title="Quitar adjunto"
                onMouseEnter={(e) => e.currentTarget.style.color = 'var(--destructive)'}
                onMouseLeave={(e) => e.currentTarget.style.color = 'var(--muted-foreground)'}
              >
                <IconX size={11} />
              </button>
            </span>
          ))}
        </div>
      )}
      {error && (
        <p className="text-[11px] font-semibold mt-1.5" style={{ color: 'var(--destructive)' }}>{error}</p>
      )}
    </div>
  );
}

// ── Comprobantes del ERP → PDF adjuntable ──────────────────────────────────
// La OC del cliente queda AFUERA a propósito: no existe renderer imprimible de
// OC-a-proveedor (solo el doc de OC del expediente en pantalla) — listar algo
// que no podemos generar sería un botón de mentira.
function _crmComprobantesDe(exp) {
  const rows = [];
  const fecha = window.formatDate || ((d) => d);
  if (exp.presupuesto) {
    rows.push({
      label: 'Presupuesto', codigo: exp.presupuesto.publicId || `N° ${exp.presupuesto.numero || ''}`,
      fecha: fecha(exp.presupuesto.fechaEmision), doc: exp.presupuesto,
      mod: () => window.MicosDocPresupuesto, archivo: `Presupuesto-${exp.publicId}.pdf`, dot: 'var(--pipeline-presupuesto)',
    });
  }
  if (exp.remito) {
    rows.push({
      label: 'Remito', codigo: exp.remito.publicId || `N° ${exp.remito.numero || ''}`,
      fecha: fecha(exp.remito.fechaCreacion || exp.remito.fechaEmision), doc: exp.remito,
      mod: () => window.MicosDocRemito, archivo: `Remito-${exp.publicId}.pdf`, dot: 'var(--pipeline-remito)',
    });
  }
  (Array.isArray(exp.facturas) ? exp.facturas : []).forEach((f) => {
    rows.push({
      label: `Factura ${f.tipoFactura || ''}`.trim(), codigo: _crmCodigoFactura(f),
      fecha: fecha(f.fechaEmision), doc: f,
      mod: () => (f.tipoFactura === 'A' ? window.MicosDocFacturaA : window.MicosDocFacturaB),
      archivo: `Factura-${_crmCodigoFactura(f) || exp.publicId}.pdf`, dot: 'var(--pipeline-factura)',
    });
  });
  if (exp.recibo) {
    rows.push({
      label: 'Recibo de cobro', codigo: exp.recibo.numeroComprobante || exp.recibo.publicId || '',
      fecha: fecha(exp.recibo.fechaCobro), doc: exp.recibo,
      mod: () => window.MicosDocRecibo, archivo: `Recibo-${exp.publicId}.pdf`, dot: 'var(--pipeline-recibo)',
    });
  }
  return rows;
}

// Rasteriza el SVG del renderer a un PDF y devuelve el base64 (sin descargar).
// Réplica de MicosDocCommon.downloadPDF, que hace pdf.save() y no devuelve los
// bytes — para el fixer: extraer una variante "toBase64" a print/doc-common.js
// y borrar esta copia (mismo canvas 3x, misma fuente embebida).
function _crmPdfBase64(mod, data) {
  const jsPDFCtor = window.jspdf && window.jspdf.jsPDF;
  const common = window.MicosDocCommon;
  if (!jsPDFCtor || !common || !mod || !mod.buildSVG) {
    return Promise.reject(new Error('El generador de PDF no está cargado.'));
  }
  const W = 595, H = 842, SCALE = 3;
  return common.ensureFontCss().then((fontCss) => new Promise((resolve, reject) => {
    const svg = mod.buildSVG(data);
    if (fontCss) {
      const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
      style.textContent = fontCss;
      svg.insertBefore(style, svg.firstChild);
    }
    svg.setAttribute('width', String(W));
    svg.setAttribute('height', String(H));
    const xml = new XMLSerializer().serializeToString(svg);
    const url = URL.createObjectURL(new Blob([xml], { type: 'image/svg+xml;charset=utf-8' }));
    const img = new Image();
    img.onload = () => {
      try {
        const canvas = document.createElement('canvas');
        canvas.width = W * SCALE; canvas.height = H * SCALE;
        const ctx = canvas.getContext('2d');
        ctx.fillStyle = '#fff'; ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        URL.revokeObjectURL(url);
        const png = canvas.toDataURL('image/png');
        const pdf = new jsPDFCtor({ unit: 'mm', format: 'a4', orientation: 'portrait', compress: true });
        pdf.addImage(png, 'PNG', 0, 0, 210, 297);
        resolve(pdf.output('datauristring').split(',')[1]);
      } catch (e) { reject(e); }
    };
    img.onerror = (e) => { URL.revokeObjectURL(url); reject(e); };
    img.src = url;
  }));
}

// Modal: elegir un comprobante REAL del expediente vinculado y adjuntarlo como
// PDF (auditoría #20/#30 + acción rápida "Adjuntar presupuesto del ERP").
// `preferido` (opcional, ej. 'Presupuesto') auto-elige esa fila al cargar.
function CrmPickerComprobantes({ open, expRef, onClose, onPick, preferido }) {
  const [exp, setExp] = _useStateEV(null);
  const [cargando, setCargando] = _useStateEV(false);
  const [generando, setGenerando] = _useStateEV(null);
  const [error, setError] = _useStateEV(null);

  _useEffectEV(() => {
    if (!open || !expRef) return;
    setCargando(true); setError(null); setExp(null); setGenerando(null);
    let vivo = true;
    crmDetalleExpediente(expRef).then((e) => {
      if (!vivo) return;
      setCargando(false);
      if (!e) setError('No se pudo cargar el expediente vinculado.');
      else setExp(e);
    });
    return () => { vivo = false; };
  }, [open, expRef]);

  const elegir = (row) => {
    if (generando) return;
    setGenerando(row.archivo); setError(null);
    const co = window.COMPANY_CONFIG || {};
    let data;
    try {
      const mod = row.mod();
      data = mod && mod.fromExpediente ? mod.fromExpediente(exp, co, row.doc) : null;
      if (!data) throw new Error('sin renderer');
      _crmPdfBase64(mod, data).then((dataBase64) => {
        const sizeBytes = Math.round(dataBase64.length * 3 / 4);
        onPick({ nombre: row.archivo, tipo: 'pdf', mime: 'application/pdf', sizeBytes, origen: 'erp', dataBase64 });
        setGenerando(null);
        onClose();
      }).catch(() => { setGenerando(null); setError(`No se pudo generar el PDF de ${row.label}.`); });
    } catch (e) {
      setGenerando(null); setError(`No se pudo generar el PDF de ${row.label}.`);
    }
  };

  // Con `preferido` (acción rápida del panel o ?doc= del deep-link): si la
  // fila existe, se adjunta directo sin pedir otro click — menos pasos
  // (criterio rector). Matchea por label ("Presupuesto") o por código/publicId.
  _useEffectEV(() => {
    if (!open || !exp || !preferido || generando) return;
    const q = String(preferido).toLowerCase();
    const row = _crmComprobantesDe(exp).find((r) =>
      r.label.toLowerCase() === q || String(r.codigo || '').toLowerCase() === q
    );
    if (row) elegir(row);
  }, [open, exp]);

  if (!open) return null;
  const rows = exp ? _crmComprobantesDe(exp) : [];

  return (
    <div
      className="fixed inset-0 z-[60] flex items-center justify-center p-4"
      style={{ background: 'color-mix(in oklab, #0a1418 62%, transparent)', backdropFilter: 'blur(4px)' }}
      onMouseDown={(e) => { if (e.target === e.currentTarget) onClose(); }}
    >
      <div
        className="w-full max-w-md rounded-2xl animate-fade-in-up"
        style={{ background: 'var(--card)', border: '1px solid color-mix(in oklab, var(--border) 65%, transparent)', boxShadow: '0 30px 80px -20px rgba(8, 20, 24, 0.55)' }}
        onMouseDown={(e) => e.stopPropagation()}
      >
        <div className="flex items-start justify-between gap-4 px-5 pt-4 pb-3" style={{ borderBottom: '1px solid color-mix(in oklab, var(--border) 55%, transparent)' }}>
          <div className="min-w-0">
            <p className="text-[10.5px] font-bold uppercase tracking-[0.14em]" style={{ color: 'var(--accent)' }}>Adjuntar del ERP</p>
            <h2 className="text-[15px] font-extrabold tracking-tight mt-0.5" style={{ color: 'var(--foreground)' }}>
              {exp ? `Comprobantes de ${exp.publicId}` : 'Comprobantes del expediente'}
            </h2>
          </div>
          <button type="button" onClick={onClose} className="cursor-pointer flex-shrink-0" style={{ color: 'var(--muted-foreground)' }} title="Cerrar">
            <IconX size={16} />
          </button>
        </div>
        <div className="px-2 py-2" style={{ maxHeight: '50vh', overflowY: 'auto' }}>
          {cargando && (
            <p className="px-3 py-6 text-center text-[12.5px]" style={{ color: 'var(--muted-foreground)' }}>Cargando comprobantes…</p>
          )}
          {!cargando && exp && rows.length === 0 && (
            <p className="px-3 py-6 text-center text-[12.5px]" style={{ color: 'var(--muted-foreground)' }}>
              {`${exp.publicId} todavía no tiene comprobantes emitidos.`}
            </p>
          )}
          {rows.map((row) => (
            <button
              key={row.archivo}
              type="button"
              onClick={() => elegir(row)}
              disabled={!!generando}
              className="w-full flex items-center gap-3 rounded-xl px-3 py-2.5 text-left transition-colors cursor-pointer"
              style={{ background: 'transparent', opacity: generando && generando !== row.archivo ? 0.5 : 1 }}
              onMouseEnter={(e) => e.currentTarget.style.background = 'color-mix(in oklab, var(--surface) 60%, transparent)'}
              onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
            >
              <span aria-hidden="true" className="w-2 h-2 rounded-full flex-shrink-0" style={{ background: row.dot }} />
              <span className="min-w-0 flex-1">
                <span className="block text-[13px] font-bold" style={{ color: 'var(--foreground)' }}>
                  {generando === row.archivo ? 'Generando PDF…' : row.label}
                </span>
                <span className="block text-[11px] font-mono" style={{ color: 'var(--muted-foreground)' }}>
                  {[row.codigo, row.fecha].filter(Boolean).join(' · ')}
                </span>
              </span>
              <IconChevronRight size={13} style={{ color: 'var(--muted-foreground)', flexShrink: 0 }} />
            </button>
          ))}
          {error && (
            <p className="px-3 py-2 text-[11.5px] font-semibold" style={{ color: 'var(--destructive)' }}>{error}</p>
          )}
        </div>
      </div>
    </div>
  );
}

// ── Responder a todos / Reenviar — cálculo de destinatarios (#17/#18) ──────
function _crmCasillasPropias(user) {
  const set = new Set(['no-reply@micossrl.com']);
  if (user && user.email) set.add(String(user.email).toLowerCase());
  (window.CRM_ALIASES || []).forEach((a) => set.add(String(a.email).toLowerCase()));
  return set;
}
function _crmDedupe(list) {
  const seen = new Set();
  return list.filter((e) => {
    const k = String(e || '').toLowerCase();
    if (!k || seen.has(k)) return false;
    seen.add(k);
    return true;
  });
}
// `todos` = responder-a-todos. Base: el último mensaje ENTRANTE (si el hilo lo
// tiene); si no —hilo iniciado por nosotros— se responde a los destinatarios
// del último enviado. Siempre se filtran las casillas propias (usuario + los 3
// alias + no-reply) para no auto-mandarnos el mail.
function crmDestinatariosRespuesta(email, user, todos) {
  const msgs = (email && email.messages) || [];
  const lastIn = [...msgs].reverse().find((m) => m.direccion === 'entrada');
  const base = lastIn || msgs[msgs.length - 1] || null;
  if (!base) return { para: [], cc: [] };
  const propias = _crmCasillasPropias(user);
  const ajeno = (e) => e && !propias.has(String(e).toLowerCase());
  if (!todos) {
    if (lastIn) return { para: [lastIn.from.email].filter(Boolean), cc: [] };
    return { para: _crmDedupe((base.to || []).filter(ajeno)), cc: [] };
  }
  let para = _crmDedupe([base.from && base.from.email, ...(base.to || [])].filter(ajeno));
  // Caso borde: todos los destinatarios eran casillas nuestras (mail interno)
  // — mejor precargar los originales que dejar el "Para" vacío.
  if (!para.length) para = _crmDedupe((base.to || []).filter(Boolean));
  const cc = _crmDedupe((base.cc || []).filter(ajeno)).filter((e) => !para.some((p) => p.toLowerCase() === e.toLowerCase()));
  return { para, cc };
}
// Cuerpo citado del reenvío (#18) — mismo formato que cualquier cliente de mail.
function crmCitaReenvio(msg, asunto) {
  const d = new Date(msg.fecha);
  const fecha = `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
  return [
    '', '',
    '---------- Mensaje reenviado ----------',
    `De: ${msg.from.nombre} <${msg.from.email}>`,
    `Fecha: ${fecha}`,
    `Asunto: ${asunto || ''}`,
    `Para: ${(msg.to || []).join(', ')}`,
    '',
    msg.cuerpo || '',
  ].join('\n');
}

Object.assign(window, {
  crmDetalleExpediente, crmInvalidarDetalle, crmPrepararPlantilla, crmAplicarVariables,
  CrmPlantillasDropdown, useCrmBorrador, CrmEstadoBorrador,
  useCrmAdjuntosSalientes, CrmChipsAdjuntos, CrmPickerComprobantes, crmPayloadAdjuntos,
  crmDestinatariosRespuesta, crmCitaReenvio, crmParseEmails,
});
