// messages.jsx — Hub de Mensagens Horos (Evolution API + Supabase)

/* ── Helpers ────────────────────────────────────────────────────────────── */

function _paletteFor(phone) {
  const s = phone || 'x';
  let h = 5381;
  for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0;
  return `hsl(${((h >>> 0) % 360)}, 52%, 40%)`;
}

function _fmtTs(ts) {
  if (!ts) return '';
  const d = new Date(ts), now = new Date();
  const diffMin = (now - d) / 60000;
  if (diffMin < 1)  return 'agora';
  if (diffMin < 60) return `${Math.floor(diffMin)}min`;
  if (d.toDateString() === now.toDateString())
    return d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
  return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' });
}

function _fmtMsgTime(ts) {
  if (!ts) return '';
  return new Date(ts).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
}

function _dateSepLabel(ts) {
  const d = new Date(ts), now = new Date();
  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
  const day   = new Date(d.getFullYear(),   d.getMonth(),   d.getDate());
  const diff  = (today - day) / 86400000;
  if (diff === 0) return 'Hoje';
  if (diff === 1) return 'Ontem';
  if (diff < 7)   return d.toLocaleDateString('pt-BR', { weekday: 'long' });
  return d.toLocaleDateString('pt-BR', { day: '2-digit', month: 'short', year: 'numeric' });
}

function _msgDayKey(ts) {
  const d = new Date(ts || 0);
  return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
}

function _cleanUrl(url) { return url.replace(/[.,);:!?'"]+$/, ''); }

function _displayUrl(url) {
  try {
    const u = new URL(_cleanUrl(url));
    const full = u.hostname + u.pathname + u.search;
    return full.length > 46 ? full.slice(0, 44) + '…' : full;
  } catch { return url.slice(0, 46); }
}

function _maskPhone(digits) {
  const d = (digits || '').replace(/\D/g, '');
  if (d.length === 11) return `(${d.slice(0,2)}) ${d.slice(2,7)}-${d.slice(7)}`;
  if (d.length === 10) return `(${d.slice(0,2)}) ${d.slice(2,6)}-${d.slice(6)}`;
  return d;
}

// Paleta de etiquetas do WhatsApp Business — 20 cores, índice = label.color (0–19)
const _WA_COLORS = [
  '#ff9485','#64c4ff','#ffd429','#dfaef0','#99b6c1','#55ccb3','#ff9dff','#d3a91d','#6d7cce','#d7e752',
  '#00d0e2','#ffc5c7','#93ceac','#f74848','#00a0f2','#83e422','#ffaf04','#b5ebff','#9ba6ff','#9368cf',
];
function _waLabelColor(idx) { return _WA_COLORS[((idx || 0) >>> 0) % _WA_COLORS.length]; }

// Listas do sistema do WhatsApp (não aparecem como etiqueta no contato): "Não lidas" e "Grupos" são
// filtros dinâmicos; "Favoritos" filtra por associação. Nome pode vir sem acento (Evolution corta
// não-ASCII), então compara normalizado.
function _waSystemList(l) {
  const n = String(l?.name || '').normalize('NFD').replace(/[^a-zA-Z ]/g, '').trim().toLowerCase();
  if (/^(nao lidas|no lidas|unread)$/.test(n)) return 'unread';
  if (/^(grupos|groups)$/.test(n))             return 'groups';
  if (/^(favoritos|favoritas|favorites)$/.test(n)) return 'favorites';
  return null;
}
function _waLabelDot(l) { return _waSystemList(l) ? 'var(--fg-3)' : _waLabelColor(l.color); }
// Ordem do WhatsApp: listas do sistema primeiro (Não lidas, Favoritos, Grupos — como no app),
// depois as etiquetas na ordem de criação (ID numérico). O Baileys não repassa reordenação manual.
const _WA_SYS_ORDER = { unread: 0, favorites: 1, groups: 2 };
function _sortWaLabels(list) {
  const rank = l => { const s = _waSystemList(l); return s ? _WA_SYS_ORDER[s] : 3; };
  return [...(list || [])].sort((a, b) =>
    (rank(a) - rank(b)) || ((parseInt(a.label_id, 10) || 0) - (parseInt(b.label_id, 10) || 0)));
}
// Etiquetas atribuíveis a uma conversa (exclui listas dinâmicas do sistema)
function _waTagLabels(list) { return (list || []).filter(l => !_waSystemList(l)); }

function _mapConv(c) {
  const jidPart   = (c.wa_jid || '').split('@')[0];
  const jidDigits = jidPart.replace(/\D/g, '');
  // LID aparece como @lid OU como número >13 dígitos em @s.whatsapp.net
  // Telefones BR têm 10-11 dígitos; internacionais chegam a 13. LIDs têm 14-18.
  const isLidContact = (c.wa_jid || '').endsWith('@lid') || jidDigits.length > 13;

  const rawName    = c.contact_name;
  const _FAKE_NAMES = new Set(['desconhecido', 'unknown', 'contato wa', 'contato']);
  const isRealName = rawName && !/^[0-9]{10,}$/.test(rawName) && rawName !== jidPart
    && !_FAKE_NAMES.has(rawName.trim().toLowerCase());

  // Prefere contact_phone; se nulo, tenta extrair do JID (exceto LID — não é número real)
  const rawPhone    = ((c.contact_phone || (!isLidContact ? jidDigits : '')) || '').replace(/\D/g, '');
  const isRealPhone = rawPhone.length >= 10 && rawPhone.length <= 13;

  const isGroup = (c.wa_jid || '').endsWith('@g.us');
  const fallbackName = isGroup
    ? 'Grupo'
    : (isRealPhone
      ? (window.fmtPhone ? window.fmtPhone(rawPhone) : rawPhone)
      // LID sem nome: sufixo do LID diferencia contatos na lista (LID não é telefone)
      : (isLidContact ? `Contato ·${jidDigits.slice(-4)}` : (jidPart || 'Contato WA')));

  const displayName = isRealName ? rawName : fallbackName;
  const unnamed     = !isRealName && !isGroup && !isRealPhone;

  // BUG 1 FIX: normalise labels to strings so filter comparisons always work
  // Supabase may return numeric IDs (PostgreSQL int) or string IDs depending on
  // column type. Converting here guarantees c.labels.includes(labelId:string) works.
  const rawLabels = Array.isArray(c.labels) ? c.labels : [];
  const labels    = rawLabels.map(l => String(l));

  return {
    id:       c.id,
    jid:      c.wa_jid || '',
    name:     displayName,
    unnamed,
    phone:    isRealPhone ? rawPhone : (isLidContact || isGroup ? '' : jidPart || ''),
    last:       c.last_message || '',
    lastFromMe: !!c.last_message_from_me,
    ts:         c.last_message_at || new Date(0).toISOString(),
    unread:   c.unread_count || 0,
    pic:      c.profile_pic_url || null,
    lastInboundAt: c.last_inbound_at || null, // WhatsApp oficial: janela de 24 h
    labels,
    pinned:      !!c.pinned,
    archived:    !!c.archived,
    typing:      !!c.typing_at && (Date.now() - new Date(c.typing_at).getTime() < 5000),
    intellStatus: c.intelligence_status || 'IDLE',
    convVersion:  c.conversation_version ?? null,
  };
}

function _mapMsg(m) {
  return {
    id:     m.id,
    waId:   m.wa_message_id || null,
    convId: m.conversation_id,
    fromMe: m.direction === 'out',
    type:   m.type || 'text',
    body:   m.body || '',
    status: m.status || 'sent',
    sentAt: m.sent_at || m.created_at || new Date(0).toISOString(),
    payload: m.payload || {},
    reactions: Array.isArray(m.payload?.reactions) ? m.payload.reactions : [],
  };
}

// Junta mensagens vindas do banco (Realtime, fallback, sync) às que estão na tela sem duplicar.
// A mensagem otimista do envio (id 'opt-N') não tem o id do banco — antes, os caminhos que só
// comparavam por id adicionavam a versão real AO LADO dela (bolha repetida).
// Casamento, em ordem: mesmo id · mesmo waId · otimista ainda sem waId do mesmo tipo/texto, <2min.
function _keepLocalMedia(opt, m) {
  return opt.payload?.localBase64
    ? { ...m, payload: { ...(m.payload || {}), localBase64: opt.payload.localBase64, localMime: opt.payload.localMime } }
    : m;
}
// ── Grupos: quem enviou cada mensagem (payload.sender = { jid, phone, name }) ──
const _SENDER_COLORS = ['#06cf9c','#53bdeb','#ff7eb6','#ffbc38','#a791ff','#fc9775','#42c7b8','#e26ab6','#7fc15e','#ff8a80','#5fb0ff','#d3a91d'];
function _senderColor(key) {
  let h = 0; for (const c of String(key || '')) h = (h * 31 + c.charCodeAt(0)) >>> 0;
  return _SENDER_COLORS[h % _SENDER_COLORS.length];
}
function _fmtIntlPhone(d) {
  d = String(d || '').replace(/\D/g, '');
  if (d.startsWith('55') && (d.length === 12 || d.length === 13)) {
    const n = d.slice(4);
    return `+55 ${d.slice(2, 4)} ${n.length === 9 ? n.slice(0, 5) + '-' + n.slice(5) : n.slice(0, 4) + '-' + n.slice(4)}`;
  }
  return d ? '+' + d : '';
}
// { label, sub, color }: nome da agenda (conversa salva) → senão número + "~nome do perfil"
function _senderInfo(sender, phoneNames) {
  if (!sender) return null;
  const digits = String(sender.phone || '').replace(/\D/g, '');
  const key = digits.length > 11 && digits.startsWith('55') ? digits.slice(2) : digits;
  const saved = key ? phoneNames.get(key) : null;
  const color = _senderColor(sender.jid || digits || sender.name);
  if (saved) return { label: saved, sub: null, color };
  if (digits) return { label: _fmtIntlPhone(digits), sub: sender.name ? '~' + sender.name : null, color };
  return { label: sender.name ? '~' + sender.name : 'Participante', sub: null, color };
}

function _mergeMsgs(prev, incoming) {
  const out = [...prev];
  for (const m of incoming) {
    const i = out.findIndex(x => x.id === m.id || (m.waId && x.waId === m.waId));
    if (i !== -1) {
      if (String(out[i].id).startsWith('opt-')) out[i] = _keepLocalMedia(out[i], m);
      else if (m.payload?.sender && !out[i].payload?.sender) out[i] = { ...out[i], payload: { ...(out[i].payload || {}), sender: m.payload.sender } };
      continue;
    }
    if (m.fromMe) {
      const j = out.findIndex(x => String(x.id).startsWith('opt-') && !x.waId
        && x.convId === m.convId && x.type === m.type
        && (m.type !== 'text' || (x.body || '').trim() === (m.body || '').trim())
        && Math.abs(new Date(x.sentAt) - new Date(m.sentAt)) < 120000);
      if (j !== -1) { out[j] = _keepLocalMedia(out[j], m); continue; }
    }
    out.push(m);
  }
  return out.sort((a, b) => new Date(a.sentAt) - new Date(b.sentAt));
}

/* ── Ícones locais (não disponíveis em icons.jsx) ───────────────────────── */

const _MI = {
  mic:      <g><rect x="9" y="2.5" width="6" height="12" rx="3" fill="currentColor" fillOpacity={0.18} /><path d="M19 11a7 7 0 0 1-14 0M12 18v4M8 22h8" /></g>,
  stop:     <rect x="6" y="6" width="12" height="12" rx="2" fill="currentColor" />,
  play_btn: <polygon points="5,3 19,12 5,21" fill="currentColor" />,
  pause_btn:<g><rect x="6" y="4" width="4" height="16" fill="currentColor" /><rect x="14" y="4" width="4" height="16" fill="currentColor" /></g>,
  smile:    <g><circle cx="12" cy="12" r="9" fill="currentColor" fillOpacity={0.18} /><path d="M9 14s1 2 3 2 3-2 3-2M9 9h.01M15 9h.01" /></g>,
  img:      <g><rect x="3" y="3" width="18" height="18" rx="2" fill="currentColor" fillOpacity={0.18} /><circle cx="8.5" cy="8.5" r="1.5" fill="currentColor" /><path d="m21 15-5-5L5 21" /></g>,
  file:     <g><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="currentColor" fillOpacity={0.18} /><polyline points="14 2 14 8 20 8" /><line x1="8" y1="13" x2="16" y2="13" /><line x1="8" y1="17" x2="16" y2="17" /></g>,
  download: <g><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3" /></g>,
  copy:     <g><rect x="9" y="9" width="13" height="13" rx="2" fill="currentColor" fillOpacity={0.18} /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></g>,
  clip:     <g><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48" /></g>,
  xmark:    <path d="M18 6 6 18M6 6l12 12" />,
  check:    <polyline points="20 6 9 17 4 12" />,
  tick2:    <g><polyline points="17 6 9 16 4 12"/><polyline points="22 6 13 16" /></g>,
  ban:      <g><circle cx="12" cy="12" r="9" fill="currentColor" fillOpacity={0.18}/><path d="m4.9 4.9 14.2 14.2" /></g>,
  alert:    <g><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" fill="currentColor" fillOpacity={0.18}/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></g>,
  pin:      <g><line x1="12" y1="17" x2="12" y2="22"/><path d="M5 17h14v-1.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V6h1a2 2 0 0 0 0-4H8a2 2 0 0 0 0 4h1v4.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24Z" fill="currentColor" fillOpacity={0.18}/></g>,
  reply:    <path d="M9 10h4a4 4 0 0 1 4 4v2M9 10l-4 3 4 3" />,
  qr:       <g><rect x="3" y="3" width="7" height="7" rx="1" fill="currentColor" fillOpacity={0.18} /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="3" height="3" rx=".5" /><rect x="18" y="14" width="3" height="3" rx=".5" /><rect x="14" y="18" width="3" height="3" rx=".5" /><rect x="18" y="18" width="3" height="3" rx=".5" /><rect x="5" y="5" width="3" height="3" rx=".5" fill="currentColor" /><rect x="16" y="5" width="3" height="3" rx=".5" fill="currentColor" /><rect x="5" y="16" width="3" height="3" rx=".5" fill="currentColor" /></g>,
  video:    <g><polygon points="23 7 16 12 23 17 23 7" /><rect x="1" y="5" width="15" height="14" rx="2" fill="currentColor" fillOpacity={0.18} /></g>,
  doc:      <g><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8Z" fill="currentColor" fillOpacity={0.18} /><polyline points="14 2 14 8 20 8" /></g>,
  trash:    <g><path d="M4 7h16M10 11v6M14 11v6M5 7l1 12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2l1-12M9 7V4h6v3" /></g>,
  msgs:     <g><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2Z" fill="currentColor" fillOpacity={0.18} /></g>,
  zap:      <path d="M13 2 4 14h7l-1 8 9-12h-7l1-8Z" fill="currentColor" fillOpacity={0.18} />,
  archive:  <g><rect x="2" y="4" width="20" height="5" rx="2" fill="currentColor" fillOpacity={0.18} /><path d="M4 9v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9M10 13h4" /></g>,
};
function MIcon({ name, size = 16, stroke = 1.7, color }) {
  const path = _MI[name];
  if (!path) return null;
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
      stroke={color || 'currentColor'} strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round">
      {path}
    </svg>
  );
}

/* ── Emoji data ─────────────────────────────────────────────────────────── */

const _QUICK_EMOJIS = ['👍','❤️','😂','😮','😢','🙏'];
const _EMOJI_CATS = [
  { key: 'recent', icon: '🕐', label: 'Recentes' },
  { key: 'smileys', icon: '😀', label: 'Smileys', emojis: ['😀','😃','😄','😁','😆','😅','🤣','😂','🙂','😊','😇','🥰','😍','🤩','😘','😚','😋','😛','😜','🤪','😝','🤑','🤗','🤔','🤐','🤨','😐','😑','😶','😏','😒','🙄','😬','😮','🤥','😌','😔','😪','😴','😷','🤒','🤕','🤢','🤮','🤧','🥵','🥶','🥴','😵','🤯','🤠','🥳','😎','🤓','🧐','😕','😟','🙁','☹️','😯','😲','😳','🥺','😦','😨','😥','😢','😭','😱','😖','😣','😞','😩','😫','🥱','😤','😡','😠','🤬','😈','👿','💀','💩','🤡','👹','👺','👻','👽','🤖'] },
  { key: 'gestures', icon: '👋', label: 'Gestos', emojis: ['👋','🤚','🖐️','✋','🖖','👌','🤌','✌️','🤞','🤟','🤘','🤙','👈','👉','👆','👇','☝️','👍','👎','✊','👊','🤛','🤜','👏','🙌','👐','🤲','🤝','🙏','✍️','💪','🦾'] },
  { key: 'hearts', icon: '❤️', label: 'Amor', emojis: ['❤️','🧡','💛','💚','💙','💜','🖤','🤍','🤎','💔','❣️','💕','💞','💓','💗','💖','💘','💝','💟','😻','💋','🌹','🌷','🌸','🌺','🌻','💐','🎀','🎁','🎊','🎉','🎆','🎇','✨','🌟','⭐','💫','🌈','☮️'] },
  { key: 'animals', icon: '🐶', label: 'Animais', emojis: ['🐶','🐱','🐭','🐹','🐰','🦊','🐻','🐼','🐨','🐯','🦁','🐮','🐷','🐸','🐵','🙈','🙉','🙊','🐔','🐧','🐦','🦅','🦆','🦉','🦇','🐺','🐴','🦄','🐝','🦋','🐌','🐞','🐢','🐍','🦎','🐙','🦑','🐡','🐠','🐟','🐬','🐳','🦈','🐊','🐅','🐆','🦓','🐘','🦒','🦘','🐃','🐄','🐎','🐑','🦙','🐕','🐈','🐓'] },
  { key: 'food', icon: '🍎', label: 'Comida', emojis: ['🍎','🍊','🍋','🍇','🍓','🍑','🍒','🍌','🍍','🥭','🍉','🥝','🍅','🥥','🥑','🍆','🥕','🌽','🌶️','🥒','🥦','🧄','🍄','🥜','🍞','🥐','🥨','🧀','🍖','🍗','🥚','🍳','🥞','🍔','🍟','🌭','🍕','🌮','🌯','🍜','🍝','🍣','🍛','🍲','🥗','🍿','🍰','🎂','🍩','🍦','☕','🍺','🥤','🧋','💧'] },
  { key: 'travel', icon: '🚗', label: 'Viagem', emojis: ['🚗','🚕','🚙','🏎️','🚓','🚑','🚒','🚐','🛻','🚜','🏍️','🛵','🚲','✈️','🛫','🛬','🚀','🛸','🚁','🛶','⛵','🚤','🏠','🏢','🏥','🏦','🏛️','⛩️','⛪','🏔️','🌋','🏝️','🌊','🌅','🌄','🌃','🌆','🌇','🌉','🗺️','🗼','🗽'] },
  { key: 'objects', icon: '💡', label: 'Objetos', emojis: ['💡','🔦','📱','💻','⌨️','🖥️','💾','💿','📷','📹','🎥','📞','☎️','📺','📻','⏰','⌚','📡','🔋','🔌','💰','💳','✉️','📧','📦','📝','✏️','🖊️','📖','📚','📊','📈','📉','🗓️','📁','📂','🔑','🗝️','🔒','🔓','🔨','🔧','🔩','💊','🔬','🔭','🎯','🎮','🕹️','🎲','🃏','♟️'] },
];
const _RECENT_KEY = 'horos-emoji-recents';
function _getRecents() { try { return JSON.parse(localStorage.getItem(_RECENT_KEY) || '[]'); } catch { return []; } }
function _addRecent(e) {
  const list = [e, ..._getRecents().filter(r => r !== e)].slice(0, 32);
  try { localStorage.setItem(_RECENT_KEY, JSON.stringify(list)); } catch {}
}

/* ── Avatar ─────────────────────────────────────────────────────────────── */

function MsgAvatar({ name, pic, size = 40, phone, style: ex }) {
  const bg = _paletteFor(phone || name || '');
  if (pic) return (
    <img src={pic} alt={name || ''}
      style={{ width: size, height: size, borderRadius: '50%', objectFit: 'cover', flexShrink: 0, ...(ex||{}) }} />
  );
  const ini = typeof initials === 'function' ? initials(name || '') : (name || '?').slice(0, 2).toUpperCase();
  return (
    <div style={{ width: size, height: size, borderRadius: '50%', background: bg, display: 'grid',
      placeItems: 'center', flexShrink: 0, color: '#fff', fontWeight: 600,
      fontSize: Math.round(size * 0.36), letterSpacing: '-0.02em', ...(ex||{}) }}>
      {ini}
    </div>
  );
}

/* ── LinkWithCopy ────────────────────────────────────────────────────────── */

function _LinkWithCopy({ url }) {
  const [copied, setCopied] = React.useState(false);
  function copy(e) {
    e.stopPropagation();
    navigator.clipboard.writeText(url).catch(() => {});
    setCopied(true);
    setTimeout(() => setCopied(false), 1800);
  }
  return (
    <span className="mc-link-wrap">
      <a href={url} target="_blank" rel="noopener noreferrer" className="mc-msg-link"
        onClick={e => e.stopPropagation()}>{_displayUrl(url)}</a>
      <button className="mc-link-copy" onClick={copy} title="Copiar">{copied ? <MIcon name="check" size={11} /> : <MIcon name="copy" size={11} />}</button>
    </span>
  );
}

function _linkify(text) {
  return (text || '').split(/(https?:\/\/[^\s]+)/g).map((p, i) => {
    if (/^https?:\/\//.test(p)) return <_LinkWithCopy key={i} url={_cleanUrl(p)} />;
    return p;
  });
}

/* ── Ticks ───────────────────────────────────────────────────────────────── */

function _Ticks({ status }) {
  if (status === 'failed')    return <span className="mc-ticks" title="Falhou ao enviar" style={{ color: '#ef4444' }}><MIcon name="xmark" size={13} /></span>;
  if (status === 'read')      return <span className="mc-ticks read" title="Lido"><MIcon name="tick2" size={13} /></span>;
  if (status === 'delivered') return <span className="mc-ticks" title="Entregue"><MIcon name="tick2" size={13} /></span>;
  return <span className="mc-ticks" title="Enviado"><MIcon name="check" size={13} /></span>;
}

/* ── EmojiPicker ─────────────────────────────────────────────────────────── */

function _EmojiPicker({ onSelect, onClose, anchorRef, style: ex }) {
  const [cat, setCat]   = React.useState('recent');
  const [recs, setRecs] = React.useState(() => _getRecents());

  function pick(e) { _addRecent(e); setRecs(_getRecents()); onSelect(e); }

  React.useEffect(() => {
    function h(e) { if (e.key === 'Escape') onClose(); }
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, []);

  const cur = cat === 'recent'
    ? recs
    : (_EMOJI_CATS.find(c => c.key === cat)?.emojis || []);

  return (
    <div className="mc-emoji-picker" style={ex}>
      <div className="mc-emoji-cats">
        {_EMOJI_CATS.map(c => (
          <button key={c.key} className={`mc-emoji-cat-btn${cat === c.key ? ' active' : ''}`}
            onClick={() => setCat(c.key)} title={c.label}>{c.icon}</button>
        ))}
      </div>
      <div className="mc-emoji-grid">
        {cur.length === 0 && cat === 'recent' && (
          <div style={{ width: '100%', padding: '20px 8px', textAlign: 'center', color: 'var(--fg-4)', fontSize: 12 }}>
            Nenhum emoji usado ainda
          </div>
        )}
        {cur.map((e, i) => <button key={i} className="mc-emoji-btn" onClick={() => pick(e)}>{e}</button>)}
      </div>
    </div>
  );
}

/* ── Mídia — lazy load ───────────────────────────────────────────────────── */

function _useMedia(waId) {
  const [src, setSrc]         = React.useState(null);
  const [mime, setMime]       = React.useState(null);
  const [loading, setLoading] = React.useState(!!waId);
  const [error, setError]     = React.useState(false);
  const [retryKey, setRetryKey] = React.useState(0);

  React.useEffect(() => {
    if (!waId) { setLoading(false); return; }
    const tok = window.HOROS_SESSION?.access_token;
    setLoading(true);
    setError(false);
    setSrc(null);
    fetch(`/api/whatsapp-media?msgId=${encodeURIComponent(waId)}`, {
      headers: { 'Authorization': `Bearer ${tok}` },
    })
      .then(r => r.ok ? r.json() : Promise.reject(r.status))
      .then(d => { setSrc(d.base64 || d.url || null); setMime(d.mimetype || null); setLoading(false); })
      .catch(err => { console.error('[media] load failed', waId, err); setError(true); setLoading(false); });
  }, [waId, retryKey]);

  // retryKey > 0 means a retry was just triggered; render as loading immediately
  // so the spinner appears even if the effect hasn't fired yet
  const effectiveLoading = loading || (retryKey > 0 && !src && !error);
  return { src, mime, loading: effectiveLoading, error, retry: () => { setError(false); setRetryKey(k => k + 1); } };
}

/* ── Modal de mídia (imagem / vídeo) ─────────────────────────────────────── */
function _MediaModal({ url, mime, type, caption, onClose }) {
  React.useEffect(() => {
    function onKey(e) { if (e.key === 'Escape') onClose(); }
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [onClose]);

  const isVideo = type === 'video';

  function downloadMedia(e) {
    e.stopPropagation();
    const a = document.createElement('a');
    a.href = url;
    a.download = isVideo ? 'video' : 'imagem';
    a.click();
  }

  const btnStyle = {
    background: 'rgba(255,255,255,0.13)', border: '1px solid rgba(255,255,255,0.18)',
    borderRadius: 10, height: 38, cursor: 'pointer',
    display: 'flex', alignItems: 'center', gap: 7,
    color: '#fff', fontSize: 13, fontWeight: 500,
    padding: '0 14px', transition: 'background 0.15s',
    fontFamily: 'var(--font)',
  };

  const content = (
    <div
      style={{
        position: 'fixed', inset: 0, zIndex: 99999,
        background: 'rgba(0,0,0,0.92)',
        display: 'flex', flexDirection: 'column',
        alignItems: 'center', justifyContent: 'center',
      }}
      onClick={onClose}>
      {/* Toolbar */}
      <div style={{
        position: 'absolute', top: 0, left: 0, right: 0,
        display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
        padding: '14px 20px', gap: 8,
        background: 'linear-gradient(to bottom, rgba(0,0,0,0.55) 0%, transparent 100%)',
      }} onClick={e => e.stopPropagation()}>
        <button onClick={downloadMedia} style={btnStyle}>
          <MIcon name="download" size={15} />
          Baixar
        </button>
        <button onClick={e => { e.stopPropagation(); onClose(); }} style={btnStyle}>
          <MIcon name="xmark" size={15} />
          Fechar
        </button>
      </div>

      {/* Conteúdo */}
      <div onClick={e => e.stopPropagation()}
        style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 14, padding: '72px 24px 32px', maxWidth: '100vw' }}>
        {isVideo ? (
          <video
            src={url} controls autoPlay
            style={{ maxWidth: '88vw', maxHeight: '78vh', borderRadius: 10, outline: 'none', background: '#000' }}
          />
        ) : (
          <img
            src={url} alt={caption || ''}
            style={{ maxWidth: '88vw', maxHeight: '78vh', objectFit: 'contain', borderRadius: 6, display: 'block' }}
          />
        )}
        {caption && (
          <div style={{ color: 'rgba(255,255,255,0.82)', fontSize: 14, textAlign: 'center', maxWidth: 560, lineHeight: 1.5 }}>
            {caption}
          </div>
        )}
      </div>
    </div>
  );

  return ReactDOM.createPortal(content, document.body);
}

function _MediaImage({ waId, caption, isSticker, localBase64: localSrc, localMime }) {
  const { src: remoteSrc, mime: remoteMime, loading, retry } = _useMedia(localSrc ? null : waId);
  const src  = localSrc ?? remoteSrc;
  const mime = localMime || remoteMime || 'image/jpeg';
  const [lightbox, setLightbox] = React.useState(false);

  if (loading) {
    if (isSticker) return (
      <div style={{ width: 96, height: 96, display: 'grid', placeItems: 'center' }}>
        <div style={{ width: 18, height: 18, border: '2px solid var(--border)', borderTopColor: 'var(--accent)', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }} />
      </div>
    );
    return (
      <div style={{ width: 220, height: 150, background: 'rgba(255,255,255,0.05)', borderRadius: 8,
        display: 'grid', placeItems: 'center' }}>
        <div style={{ width: 20, height: 20, border: '2px solid rgba(255,255,255,0.15)', borderTopColor: 'var(--accent)', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }} />
      </div>
    );
  }
  if (!src) {
    if (isSticker) return (
      <div style={{ width: 96, height: 96, display: 'grid', placeItems: 'center', opacity: 0.4,
        border: '1px dashed var(--border)', borderRadius: 12 }}>
        <MIcon name="img" size={28} />
      </div>
    );
    if (!waId) return (
      <div style={{
        width: 220, height: 120, background: 'rgba(255,255,255,0.04)', borderRadius: 10,
        display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8,
        border: '1px dashed rgba(255,255,255,0.1)', opacity: 0.5,
      }}>
        <MIcon name="img" size={22} />
        <span style={{ fontSize: 11 }}>Imagem indisponível</span>
      </div>
    );
    return (
      <div onClick={retry} style={{
        width: 220, height: 120, background: 'rgba(255,255,255,0.04)', borderRadius: 10,
        display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8,
        cursor: 'pointer', border: '1px dashed rgba(255,255,255,0.1)',
      }}>
        <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" style={{ opacity: 0.45 }}>
          <polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-3.5"/>
        </svg>
        <span style={{ fontSize: 11, opacity: 0.45 }}>Toque para tentar novamente</span>
      </div>
    );
  }
  const url = /^(data:|blob:|https?:)/.test(src) ? src : `data:${mime};base64,${src}`;
  return (
    <div className="mc-msg-image">
      <div style={{ cursor: isSticker ? 'default' : 'pointer', maxWidth: isSticker ? 120 : 300, position: 'relative' }}
        onClick={() => !isSticker && setLightbox(true)}>
        <img src={url} alt={caption || ''} style={{ maxWidth: '100%', borderRadius: isSticker ? 0 : 8, display: 'block' }} />
        {!isSticker && (
          <div style={{
            position: 'absolute', inset: 0, borderRadius: 8,
            background: 'rgba(0,0,0,0)', transition: 'background 0.15s',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }} className="mc-img-hover-overlay" />
        )}
      </div>
      {caption && <div className="mc-msg-caption">{caption}</div>}
      {lightbox && <_MediaModal url={url} type="image" caption={caption} onClose={() => setLightbox(false)} />}
    </div>
  );
}

function _MediaAudio({ waId, localBase64, localMime }) {
  const { src: remoteSrc, mime: remoteMime, loading, retry } = _useMedia(localBase64 ? null : waId);
  const src  = localBase64 ?? remoteSrc;
  const mime = localMime  ?? remoteMime;
  const [playing, setPlaying]       = React.useState(false);
  const [speed, setSpeed]           = React.useState(1);
  const [wavePeaks, setWavePeaks]   = React.useState(null);
  const [duration, setDuration]     = React.useState(0);
  const [currentTime, setCurrentTime] = React.useState(0);
  const audioRef = React.useRef();

  React.useEffect(() => {
    if (!src) return;
    try {
      const AudioCtx = window.AudioContext || window.webkitAudioContext;
      if (!AudioCtx) return;
      const context = new AudioCtx();
      const base64 = src.startsWith('data:') ? src.split(',')[1] : src;
      const binary = atob(base64);
      const buf = new Uint8Array(binary.length);
      for (let i = 0; i < binary.length; i++) buf[i] = binary.charCodeAt(i);
      context.decodeAudioData(buf.buffer).then(decoded => {
        const ch = decoded.getChannelData(0);
        const BARS = 28;
        const step = Math.floor(ch.length / BARS);
        const peaks = [];
        for (let i = 0; i < BARS; i++) {
          let max = 0;
          for (let j = 0; j < step; j++) max = Math.max(max, Math.abs(ch[i * step + j] || 0));
          peaks.push(max);
        }
        const maxPeak = Math.max(...peaks, 0.01);
        setWavePeaks(peaks.map(p => Math.max(3, Math.round((p / maxPeak) * 18))));
        context.close().catch(() => {});
      }).catch(() => {});
    } catch {}
  }, [src]);

  React.useEffect(() => {
    if (audioRef.current) audioRef.current.playbackRate = speed;
  }, [speed]);

  function fmtTime(s) {
    if (!s || isNaN(s) || !isFinite(s)) return '0:00';
    const m = Math.floor(s / 60);
    return `${m}:${Math.floor(s % 60).toString().padStart(2, '0')}`;
  }

  if (loading) return (
    <div className="mc-audio">
      <div className="mc-audio-play"><MIcon name="mic" size={14} /></div>
      <Skel h={18} w={150} r={9} />
    </div>
  );
  if (!src) return (
    <div className="mc-audio" style={{ opacity: 0.6, cursor: waId ? 'pointer' : 'default' }}
      onClick={waId ? retry : undefined}
      title={waId ? 'Clique para tentar novamente' : undefined}>
      <div className="mc-audio-play"><MIcon name="mic" size={14} /></div>
      <div style={{ fontSize: 11 }}>{waId ? 'Áudio indisponível — tentar novamente' : 'Áudio indisponível'}</div>
    </div>
  );

  const url      = /^(data:|blob:|https?:)/.test(src) ? src : `data:${mime || 'audio/ogg'};base64,${src}`;
  const BARS     = 28;
  const bars     = wavePeaks || Array.from({ length: BARS }).map((_, i) => 6 + Math.round(Math.abs(Math.sin(i * 0.7) * 9 + Math.sin(i * 1.4) * 5)));
  const progress = duration > 0 ? currentTime / duration : 0;
  const played   = Math.round(progress * BARS);

  function toggle() {
    const el = audioRef.current;
    if (!el) return;
    if (playing) { el.pause(); setPlaying(false); }
    else {
      if (window._horosCurrentAudio && window._horosCurrentAudio !== el) {
        window._horosCurrentAudio.pause();
      }
      window._horosCurrentAudio = el;
      el.playbackRate = speed;
      el.play().then(() => setPlaying(true)).catch(() => {});
    }
  }

  function seek(e) {
    const el = audioRef.current;
    if (!el || !duration) return;
    const rect  = e.currentTarget.getBoundingClientRect();
    const ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
    el.currentTime = ratio * duration;
    setCurrentTime(el.currentTime);
  }

  function cycleSpeed() {
    const next = speed === 1 ? 1.5 : speed === 1.5 ? 2 : 1;
    setSpeed(next);
  }

  return (
    <div className="mc-audio bubble-audio">
      <audio
        ref={audioRef}
        src={url}
        preload="auto"
        onLoadedMetadata={e => setDuration(e.target.duration)}
        onTimeUpdate={e => setCurrentTime(e.target.currentTime)}
        onEnded={() => { setPlaying(false); setCurrentTime(0); if (audioRef.current) audioRef.current.currentTime = 0; }}
      />
      <button className="mc-audio-play" onClick={toggle}>
        <MIcon name={playing ? 'pause_btn' : 'play_btn'} size={13} />
      </button>
      <div className="mc-audio-body">
        <div className="mc-wave" onClick={seek} style={{ cursor: duration ? 'pointer' : 'default' }}>
          {bars.map((h, i) => (
            <div key={i} className={`mc-wave-bar${i < played ? ' played' : ''}`} style={{ height: `${h}px` }} />
          ))}
        </div>
        <div className="mc-audio-footer">
          <span className="mc-audio-time">{fmtTime(playing ? currentTime : duration || currentTime)}</span>
          <button className="mc-audio-speed" onClick={cycleSpeed} title="Velocidade de reprodução">
            {speed}x
          </button>
        </div>
      </div>
    </div>
  );
}

function _MediaDocument({ waId, fileName }) {
  const [dl, setDl] = React.useState(false);
  async function download() {
    if (dl || !waId) return;
    setDl(true);
    try {
      const tok = window.HOROS_SESSION?.access_token;
      const r = await fetch(`/api/whatsapp-media?msgId=${encodeURIComponent(waId)}`, {
        headers: { 'Authorization': `Bearer ${tok}` },
      });
      const d = await r.json();
      if (!d.base64 && !d.url) return;
      const src = d.url ? d.url : d.base64.startsWith('data:') ? d.base64 : `data:application/octet-stream;base64,${d.base64}`;
      const a = document.createElement('a');
      a.href = src; a.download = fileName || 'arquivo'; a.click();
    } catch {}
    setDl(false);
  }
  return (
    <div className="mc-doc">
      <div className="mc-doc-icon"><MIcon name="doc" size={18} /></div>
      <div className="mc-doc-meta">
        <div className="mc-doc-name">{fileName || 'Documento'}</div>
        <div className="mc-doc-size">Toque para baixar</div>
      </div>
      <button className="mc-doc-dl" onClick={download} disabled={dl}>
        {dl ? '…' : <MIcon name="download" size={14} />}
      </button>
    </div>
  );
}

function _MediaVideo({ waId, caption, localBase64: localSrc, localMime }) {
  const { src: remoteSrc, mime: remoteMime, loading, retry } = _useMedia(localSrc ? null : waId);
  const src  = localSrc ?? remoteSrc;
  const mime = localMime || remoteMime || 'video/mp4';
  const [modal, setModal] = React.useState(false);

  if (loading) return (
    <div style={{ width: 260, height: 160, background: 'rgba(255,255,255,0.05)', borderRadius: 10,
      display: 'grid', placeItems: 'center' }}>
      <div style={{ width: 20, height: 20, border: '2px solid rgba(255,255,255,0.15)', borderTopColor: 'var(--accent)', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }} />
    </div>
  );
  if (!src) {
    if (!waId) return (
      <div style={{
        width: 260, height: 160, background: 'rgba(255,255,255,0.04)', borderRadius: 10,
        display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8,
        border: '1px dashed rgba(255,255,255,0.1)', opacity: 0.5,
      }}>
        <MIcon name="video" size={22} />
        <span style={{ fontSize: 11 }}>Vídeo indisponível</span>
      </div>
    );
    return (
      <div onClick={retry} style={{
        width: 260, height: 160, background: 'rgba(255,255,255,0.04)', borderRadius: 10,
        display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 8,
        cursor: 'pointer', border: '1px dashed rgba(255,255,255,0.1)',
      }}>
        <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" style={{ opacity: 0.45 }}>
          <polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-3.5"/>
        </svg>
        <span style={{ fontSize: 11, opacity: 0.45 }}>Toque para tentar novamente</span>
      </div>
    );
  }
  const url = /^(data:|blob:|https?:)/.test(src) ? src : `data:${mime};base64,${src}`;
  return (
    <div className="mc-msg-video">
      {/* Thumbnail clicável com play overlay */}
      <div style={{ position: 'relative', cursor: 'pointer', display: 'inline-block', borderRadius: 10, overflow: 'hidden', maxWidth: 280 }}
        onClick={() => setModal(true)}>
        <video src={url} muted playsInline preload="metadata"
          style={{ display: 'block', maxWidth: '100%', maxHeight: 200, borderRadius: 10, pointerEvents: 'none' }} />
        {/* Play button overlay */}
        <div style={{
          position: 'absolute', inset: 0,
          background: 'rgba(0,0,0,0.32)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          transition: 'background 0.15s',
        }}>
          <div style={{
            width: 48, height: 48, borderRadius: '50%',
            background: 'rgba(255,255,255,0.92)',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            boxShadow: '0 2px 12px rgba(0,0,0,0.3)',
          }}>
            <svg width="18" height="18" viewBox="0 0 24 24" fill="#111" style={{ marginLeft: 3 }}>
              <polygon points="5,3 19,12 5,21" />
            </svg>
          </div>
        </div>
      </div>
      {caption && <div className="mc-msg-caption">{caption}</div>}
      {modal && <_MediaModal url={url} mime={mime} type="video" caption={caption} onClose={() => setModal(false)} />}
    </div>
  );
}

/* ── MsgBubble ───────────────────────────────────────────────────────────── */

function _MsgBubble({ msg, groupStart, sender, onReact, onReply, onDelete, onForward, conv, convs }) {
  const [menuOpen, setMenuOpen]   = React.useState(false);
  const [menuFlip, setMenuFlip]   = React.useState(false);
  const [reactOpen, setReactOpen] = React.useState(false);
  const [fullPicker, setFullPicker] = React.useState(false);
  const ref = React.useRef();
  const menuRef = React.useRef();
  const isOut = msg.fromMe;

  React.useEffect(() => {
    if (!menuOpen && !reactOpen && !fullPicker) return;
    function h(e) { if (ref.current && !ref.current.contains(e.target)) {
      setMenuOpen(false); setReactOpen(false); setFullPicker(false);
    }}
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [menuOpen, reactOpen, fullPicker]);

  React.useLayoutEffect(() => {
    if (!menuOpen) { setMenuFlip(false); return; }
    if (!menuRef.current) return;
    const rect = menuRef.current.getBoundingClientRect();
    if (rect.bottom > window.innerHeight - 8) setMenuFlip(true);
  }, [menuOpen]);

  function renderBody() {
    const t = msg.type;
    // Deleted messages
    if (t === 'deleted' || t === 'deleted_local') {
      const label = t === 'deleted_local' ? 'Mensagem apagada (só para você)' : (msg.body || 'Mensagem apagada');
      return <div className="mc-msg-deleted"><MIcon name="ban" size={14} style={{marginRight:6,verticalAlign:'middle'}} />{label}</div>;
    }
    // Quoted message preview
    const quoted = msg.payload?.quoted;
    const quotedEl = quoted ? (
      <div className="mc-msg-quoted">
        <div className="mc-msg-quoted-bar" />
        <div className="mc-msg-quoted-body">
          <div className="mc-msg-quoted-author">{quoted.fromMe ? 'Você' : (String(conv?.jid || '').endsWith('@g.us') ? 'Participante' : (conv?.name || 'Contato'))}</div>
          <div className="mc-msg-quoted-text">{quoted.body || '[mídia]'}</div>
        </div>
      </div>
    ) : null;
    if (t === 'video') {
      const vidCap = (msg.body && msg.body !== '[Vídeo]' && msg.body !== '[Video]') ? msg.body : '';
      return <>{quotedEl}<_MediaVideo waId={msg.waId} caption={vidCap} localBase64={msg.payload?.localBase64} localMime={msg.payload?.localMime} /></>;
    }
    if (t === 'image' || t === 'sticker') {
      const imgCap = (t === 'sticker' || !msg.body || msg.body === '[Imagem]' || msg.body === '[Image]') ? '' : msg.body;
      return <>{quotedEl}<_MediaImage waId={msg.waId} caption={imgCap} isSticker={t === 'sticker'} localBase64={msg.payload?.localBase64} localMime={msg.payload?.localMime} /></>;
    }
    if (t === 'audio' || t === 'ptt')
      return <>{quotedEl}<_MediaAudio waId={msg.waId} localBase64={msg.payload?.localBase64} localMime={msg.payload?.localMime} /></>;
    if (t === 'document' || t === 'doc')
      return <>{quotedEl}<_MediaDocument waId={msg.waId} fileName={msg.payload?.fileName || msg.body} /></>;
    // Mensagens antigas sincronizadas antes do suporte a tipos de mídia
    if (t === 'text' && msg.body === '[Sticker]')
      return <>{quotedEl}<_MediaImage waId={msg.waId} caption="" isSticker={true} /></>;
    if (t === 'text' && (msg.body === '[Imagem]' || msg.body === '[Image]'))
      return <>{quotedEl}<_MediaImage waId={msg.waId} caption="" isSticker={false} /></>;
    if (t === 'text' && (msg.body === '[Áudio]' || msg.body === '[Audio]' || msg.body === '[Vídeo]' || msg.body === '[Video]'))
      return <>{quotedEl}<_MediaAudio waId={msg.waId} /></>;
    return <>{quotedEl}<div className="mc-msg-text">{_linkify(msg.body)}</div></>;
  }

  const bubbleClass = [
    'mc-msg-bubble',
    (msg.type === 'image' || msg.type === 'sticker') ? 'bubble-image' : '',
    (msg.type === 'audio' || msg.type === 'ptt') ? 'bubble-audio' : '',
    msg.type === 'sticker' ? 'bubble-sticker' : '',
  ].filter(Boolean).join(' ');

  const reactions = msg.reactions || [];

  return (
    <div ref={ref} className={`mc-msg-row${groupStart ? ' group-start' : ''}`} style={{ display: 'flex', alignItems: 'center', gap: 4,
      alignSelf: isOut ? 'flex-end' : 'flex-start',
      flexDirection: isOut ? 'row' : 'row-reverse', position: 'relative', maxWidth: '75%' }}>

      {/* Smiley side button — só visível no hover (CSS) ou quando picker aberto */}
      {/* row-reverse (incoming): emoji fica à direita da bolha; row (outgoing): emoji fica à esquerda */}
      <div style={{ position: 'relative' }}>
        <button className={`mc-msg-react-side${reactOpen || fullPicker ? ' active' : ''}`} onClick={() => setReactOpen(v => !v)}>
          <MIcon name="smile" size={15} />
        </button>
        {reactOpen && (
          <div className="mc-msg-menu" style={{
            position: 'absolute', bottom: '110%', [isOut ? 'left' : 'right']: 0,
            transformOrigin: isOut ? 'bottom left' : 'bottom right',
          }}>
            <div className="mc-msg-menu-reactions">
              {_QUICK_EMOJIS.map(e => (
                <button key={e} className="mc-msg-menu-emoji"
                  onClick={() => { onReact(msg, e); setReactOpen(false); }}>{e}</button>
              ))}
              <button className="mc-msg-menu-emoji-plus" onClick={() => { setReactOpen(false); setFullPicker(true); }}>+</button>
            </div>
          </div>
        )}
        {fullPicker && (
          <div className={`mc-react-full-picker${isOut ? '' : ' out'}`}>
            <_EmojiPicker onSelect={e => { onReact(msg, e); setFullPicker(false); }}
              onClose={() => setFullPicker(false)} />
          </div>
        )}
      </div>

      {/* Bubble */}
      <div style={{ display: 'flex', flexDirection: 'column', maxWidth: '100%' }}>
        <div className={`mc-msg ${isOut ? 'out' : 'in'}${groupStart ? ' tail' : ''}`} style={{ margin: 0, alignSelf: 'auto', maxWidth: '100%' }}>
          <div className={bubbleClass} style={{ position: 'relative' }}>
            <button className={`mc-msg-chevron${menuOpen ? ' active' : ''}`} onClick={() => setMenuOpen(v => !v)}>▾</button>
            {sender && (
              <div className="mc-msg-sender" style={{ color: sender.color }}>
                <span className="mc-msg-sender-name">{sender.label}</span>
                {sender.sub && <span className="mc-msg-sender-sub">{sender.sub}</span>}
              </div>
            )}
            {renderBody()}
            <div className="mc-msg-foot">
              {isOut && msg.payload?.ai_auto && <span className="mc-msg-ai" title="Enviada pelo assistente automático (o cliente não vê esta marca)">Assistente</span>}
              {isOut && msg.payload?.automation && <span className="mc-msg-ai" title="Enviada por uma automação (o cliente não vê esta marca)">Automação</span>}
              <span className="mc-msg-time">{_fmtMsgTime(msg.sentAt)}</span>
              {isOut && <_Ticks status={msg.status} />}
            </div>
          </div>
        </div>

        {reactions.length > 0 && (
          <div className="mc-msg-reactions" style={{ justifyContent: isOut ? 'flex-end' : 'flex-start' }}>
            {[...new Set(reactions.map(r => r.emoji))].map(e => (
              <button key={e} className={`mc-msg-reaction${reactions.find(r => r.emoji === e && (r.sender === 'me' || r.from === 'me')) ? ' mine' : ''}`}
                onClick={() => onReact(msg, e)}>
                {e} {reactions.filter(r => r.emoji === e).length}
              </button>
            ))}
          </div>
        )}

        {menuOpen && (
          <div ref={menuRef} className={`mc-msg-menu${menuFlip ? ' flip' : ''}`} style={{
            position: 'absolute', [isOut ? 'right' : 'left']: 0, zIndex: 300,
            transformOrigin: menuFlip ? (isOut ? 'bottom right' : 'bottom left') : (isOut ? 'top right' : 'top left'),
          }}>
            <div className="mc-msg-menu-reactions">
              {_QUICK_EMOJIS.map(e => (
                <button key={e} className="mc-msg-menu-emoji"
                  onClick={() => { onReact(msg, e); setMenuOpen(false); }}>{e}</button>
              ))}
              <button className="mc-msg-menu-emoji-plus" onClick={() => { setMenuOpen(false); setFullPicker(true); }}>+</button>
            </div>
            <div className="mc-msg-menu-sep" />
            <button className="mc-msg-menu-action" onClick={() => { onReply(msg); setMenuOpen(false); }}>
              <span className="mc-msg-menu-action-icon"><MIcon name="reply" size={15} /></span> Responder
            </button>
            <button className="mc-msg-menu-action" onClick={() => { onForward && onForward(msg); setMenuOpen(false); }}>
              <span className="mc-msg-menu-action-icon">↪</span> Encaminhar
            </button>
            <button className="mc-msg-menu-action" onClick={() => {
              navigator.clipboard.writeText(msg.body).catch(() => {});
              setMenuOpen(false);
            }}>
              <span className="mc-msg-menu-action-icon"><MIcon name="copy" size={15} /></span> Copiar
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

/* ── ForwardModal ────────────────────────────────────────────────────────── */

function _ForwardModal({ msg, convs, onClose, onForward }) {
  const [search, setSearch] = React.useState('');
  const [selectedId, setSelectedId] = React.useState(null);

  React.useEffect(() => {
    function h(e) { if (e.key === 'Escape') onClose(); }
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, []);

  const filtered = React.useMemo(() => {
    if (!search.trim()) return convs;
    const q = search.toLowerCase();
    return convs.filter(c => c.name.toLowerCase().includes(q) || c.phone.includes(q));
  }, [convs, search]);

  return (
    <div className="mc-fwd-overlay" onClick={onClose}>
      <div className="mc-fwd-modal" onClick={e => e.stopPropagation()}>
        <div className="mc-fwd-head">
          <span className="mc-fwd-title">Encaminhar mensagem</span>
          <button style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--fg-4)', lineHeight: 1 }}
            onClick={onClose}><MIcon name="xmark" size={18} /></button>
        </div>
        <div className="mc-fwd-search">
          <input placeholder="Buscar conversa…" value={search} onChange={e => setSearch(e.target.value)} autoFocus />
        </div>
        <div className="mc-fwd-list">
          {filtered.map(c => (
            <button key={c.id} className={`mc-fwd-item${selectedId === c.id ? ' selected' : ''}`}
              onClick={() => setSelectedId(c.id)}>
              <MsgAvatar name={c.name} pic={c.pic} size={34} phone={c.phone} />
              <span className="mc-fwd-name">{c.name}</span>
            </button>
          ))}
          {filtered.length === 0 && (
            <div style={{ padding: 24, textAlign: 'center', color: 'var(--fg-4)', fontSize: 13 }}>Nenhuma conversa</div>
          )}
        </div>
        <div className="mc-fwd-foot">
          <button className="mc-fwd-cancel" onClick={onClose}>Cancelar</button>
          <button className="mc-fwd-send" disabled={!selectedId}
            onClick={() => { if (selectedId) { onForward(msg, selectedId); onClose(); } }}>
            Encaminhar
          </button>
        </div>
      </div>
    </div>
  );
}

/* ── ChatBody ────────────────────────────────────────────────────────────── */

function _ChatBody({ msgs, onReact, onReply, onDelete, onForward, conv, convs, loading, bottomRef, hasMore, loadingMore, onLoadMore }) {
  const bodyRef = React.useRef(null);
  const scrollRestoreRef = React.useRef(null);
  const didLoadMore = React.useRef(false);
  const autoLoadGuardRef = React.useRef(false);
  const prevConvIdRef = React.useRef(null);
  const [showJump, setShowJump] = React.useState(false);

  // Reset guard when loading finishes
  React.useEffect(() => { if (!loadingMore) autoLoadGuardRef.current = false; }, [loadingMore]);

  function handleLoadMore() {
    const body = bodyRef.current;
    if (body) scrollRestoreRef.current = { prevScrollHeight: body.scrollHeight, prevScrollTop: body.scrollTop };
    onLoadMore?.();
  }

  function handleScroll(e) {
    const el = e.currentTarget;
    setShowJump(el.scrollHeight - el.scrollTop - el.clientHeight > 300);
    // Auto-trigger quando chega perto do topo
    if (el.scrollTop < 120 && hasMore && !loadingMore && !autoLoadGuardRef.current) {
      autoLoadGuardRef.current = true;
      handleLoadMore();
    }
  }

  // Scroll: load-more preserva posição; troca de conversa vai ao fundo instant; nova msg smooth
  React.useLayoutEffect(() => {
    // bodyRef só existe quando loading=false (a div scrollável não é renderizada no estado de spinner)
    if (loading || !bodyRef.current || !msgs.length) return;
    const convChanged = conv?.id !== prevConvIdRef.current;
    prevConvIdRef.current = conv?.id || null;

    if (scrollRestoreRef.current) {
      // Carregou mais mensagens antigas: mantém a posição relativa
      const { prevScrollHeight, prevScrollTop } = scrollRestoreRef.current;
      bodyRef.current.scrollTop = prevScrollTop + (bodyRef.current.scrollHeight - prevScrollHeight);
      scrollRestoreRef.current = null;
      didLoadMore.current = true;
    } else if (convChanged) {
      // Troca de conversa (ou loading→false com mensagens prontas): pula ao fundo antes do paint
      bodyRef.current.scrollTop = bodyRef.current.scrollHeight;
      didLoadMore.current = true;
    } else {
      didLoadMore.current = false;
    }
  }, [msgs.length, conv?.id, loading]);

  // (antes dos returns — regra dos hooks)
  const isGroupConv = String(conv?.jid || '').endsWith('@g.us');
  // Nomes da agenda (conversas salvas) por número — para mostrar o nome de quem falou no grupo
  const phoneNames = React.useMemo(() => {
    const m = new Map();
    if (!isGroupConv) return m;
    for (const c of (convs || [])) {
      if (!c || c.unnamed || String(c.jid || '').endsWith('@g.us')) continue;
      const d = String(c.phone || '').replace(/\D/g, '');
      const k = d.length > 11 && d.startsWith('55') ? d.slice(2) : d;
      if (k.length >= 10 && c.name && !/^[+\d\s()-]+$/.test(c.name)) m.set(k, c.name);
    }
    return m;
  }, [convs, isGroupConv]);

  // Nova mensagem na mesma conversa: scroll suave
  React.useEffect(() => {
    if (!didLoadMore.current) {
      bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
    }
  }, [msgs.length]);

  if (loading) return (
    <div className="mc-chat-body" role="status" aria-label="Carregando conversa" style={{ display: 'flex', flexDirection: 'column', justifyContent: 'flex-end', gap: 10, padding: '20px 6%' }}>
      {[['38%',36,0],['26%',36,1],['52%',54,0],['30%',36,1],['44%',36,0],['22%',36,1],['34%',54,1]].map(([w,h,me],i) => <Skel key={i} w={w} h={h} r={8} style={{ alignSelf: me ? 'flex-end' : 'flex-start' }} />)}
    </div>
  );

  if (!msgs.length) return (
    <div className="mc-chat-body" style={{ alignItems: 'center', justifyContent: 'center' }}>
      <div style={{ textAlign: 'center', color: 'var(--fg-4)', fontSize: 13 }}>
        <div style={{ marginBottom: 8, opacity: 0.5 }}><MIcon name="msgs" size={32} /></div>
        <div>Nenhuma mensagem ainda</div>
        <div style={{ fontSize: 12, marginTop: 4 }}>Envie uma mensagem para começar</div>
      </div>
    </div>
  );

  const items = [];
  let lastDay = null;
  let lastFromMe = null;
  let lastSender = null;
  for (const msg of msgs) {
    const day = _msgDayKey(msg.sentAt);
    const senderKey = isGroupConv && !msg.fromMe ? (msg.payload?.sender?.jid || null) : null;
    // Início de sequência (como no WhatsApp): muda o remetente ou o dia → balão com "pontinha"
    // Em grupo, cada pessoa diferente também começa uma nova sequência (com o nome dela)
    const groupStart = day !== lastDay || msg.fromMe !== lastFromMe || (isGroupConv && !msg.fromMe && senderKey !== lastSender);
    lastFromMe = msg.fromMe;
    lastSender = senderKey;
    const sender = isGroupConv && !msg.fromMe && groupStart ? _senderInfo(msg.payload?.sender, phoneNames) : null;
    if (day !== lastDay) {
      lastDay = day;
      items.push(
        <div key={`sep-${day}`} className="mc-day-sep">
          <span>{_dateSepLabel(msg.sentAt)}</span>
        </div>
      );
    }
    items.push(
      <_MsgBubble key={msg.id} msg={msg} groupStart={groupStart} sender={sender} onReact={onReact} onReply={onReply}
        onDelete={onDelete} onForward={onForward} conv={conv} convs={convs} />
    );
  }

  return (
    <div className="mc-chat-body" ref={bodyRef} onScroll={handleScroll} style={{ position: 'relative' }}>
      {hasMore && (
        <div style={{ display: 'flex', justifyContent: 'center', padding: '10px 0 6px' }}>
          <button onClick={handleLoadMore} disabled={loadingMore} style={{
            background: 'none', border: '1px solid var(--border)', borderRadius: 20,
            padding: '5px 16px', fontSize: 12, color: 'var(--fg-3)',
            cursor: loadingMore ? 'default' : 'pointer', opacity: loadingMore ? 0.55 : 1,
          }}>
            {loadingMore ? 'Carregando…' : '↑ Mensagens anteriores'}
          </button>
        </div>
      )}
      {items}
      <div ref={bottomRef} />
      {showJump && (
        <button className="mc-jump-latest" title="Ir para a última mensagem"
          onClick={() => bottomRef.current?.scrollIntoView({ behavior: 'smooth' })}>
          ↓
        </button>
      )}
    </div>
  );
}

/* ── ChatInput ───────────────────────────────────────────────────────────── */

const _PEND0 = { list: [], idx: 0, active: null };
function _pendingReducer(s, a) {
  switch (a.type) {
    case 'INIT': {
      const list = a.items;
      return { list, idx: 0, active: list[0] || null };
    }
    case 'ADD': {
      const merged = s.list.length > 0
        ? s.list.map((item, i) => i === s.idx ? { ...item, caption: a.caption || '' } : item)
        : [];
      const newList = [...merged, ...a.items];
      const newIdx  = merged.length;
      return { list: newList, idx: newIdx, active: newList[newIdx] || null };
    }
    case 'SWITCH': {
      const updated = s.list.map((item, i) => i === s.idx ? { ...item, caption: a.caption || '' } : item);
      return { list: updated, idx: a.idx, active: updated[a.idx] || null };
    }
    case 'REMOVE': {
      const next = s.list.filter((_, i) => i !== a.idx);
      if (next.length === 0) return _PEND0;
      const ni = a.idx >= next.length ? next.length - 1 : a.idx;
      return { list: next, idx: ni, active: next[ni] };
    }
    case 'RESET': return _PEND0;
    default: return s;
  }
}

const _ATTACH_OPTS = [
  { key: 'image', label: 'Fotos e vídeos', icon: 'img', accept: 'image/*,video/*', mediaType: 'image' },
  { key: 'doc',   label: 'Documento',      icon: 'doc',  accept: '*/*',             mediaType: 'document' },
  { key: 'audio', label: 'Áudio',          icon: 'mic',  accept: 'audio/*',         mediaType: 'audio' },
];

const _ChatInput = React.forwardRef(function _ChatInput({ onSend, onSendMedia, replyTo, onCancelReply, disabled, prefillText, onPrefillConsumed, contactName, convId, assOn, onToggleAss }, ref) {
  React.useImperativeHandle(ref, () => ({
    focus: () => textRef.current?.focus(),
    isPickerActive: () => Date.now() - lastPickedAtRef.current < 800,
    addFiles(files) {
      const items = [];
      for (let i = 0; i < files.length; i++) {
        const file = files[i];
        let mt = file.type.startsWith('video/') ? 'video' : file.type.startsWith('image/') ? 'image' : 'document';
        const url = (mt === 'image' || mt === 'video') ? URL.createObjectURL(file) : null;
        items.push({ file, mediaType: mt, previewUrl: url, caption: '' });
      }
      if (!items.length) return;
      if (psRef.current.list.length > 0) {
        dispatchP({ type: 'ADD', items, caption: textRef2.current.trim() });
      } else {
        dispatchP({ type: 'INIT', items });
        const el = textRef.current;
        if (el) el.style.height = 'auto';
      }
      setText('');
    },
  }));
  const [text, setText]         = React.useState('');
  const [showEmoji, setShowEmoji] = React.useState(false);
  const [showAttach, setShowAttach] = React.useState(false);
  const [ps, dispatchP] = React.useReducer(_pendingReducer, _PEND0); // { list, idx, active }
  const [recording, setRec]     = React.useState(false);
  const [mediaRec, setMediaRec] = React.useState(null);
  const [chunks, setChunks]     = React.useState([]);
  const [recSeconds, setRecSeconds] = React.useState(0);
  const [recPaused, setRecPaused]   = React.useState(false);
  const [slashSuggestions, setSlashSuggestions] = React.useState([]);
  const [slashActive, setSlashActive] = React.useState(0);
  // Respostas rápidas — botão na barra de texto
  const [showQuick, setShowQuick]   = React.useState(false);
  const [qrList, setQrList]         = React.useState(null);
  const [qrSearch, setQrSearch]     = React.useState('');
  const [qrLoading, setQrLoading]   = React.useState(false);
  const [showQrManage, setShowQrManage] = React.useState(false);
  const quickRef = React.useRef();
  const textRef = React.useRef();
  const fileRef = React.useRef();
  const pendingAccept = React.useRef('*/*');
  const pendingMedia  = React.useRef('document');
  const cancelRecRef  = React.useRef(false);
  const recIntervalRef = React.useRef(null);
  // Refs — always current inside native event handlers (stale closure safe)
  const psRef          = React.useRef(_PEND0); // mirrors ps reducer state
  const textRef2       = React.useRef('');
  // Guards spurious Enter/Escape events that some browsers fire when OS file dialog closes
  const lastPickedAtRef = React.useRef(0);
  const pickerOpenRef   = React.useRef(false); // true while OS file dialog is open

  // Keep refs in sync so native file-picker change handlers never read stale closures
  React.useEffect(() => { psRef.current   = ps;   }, [ps]);
  React.useEffect(() => { textRef2.current = text; }, [text]);

  // Refresh the Enter/Escape guard the instant the browser window regains focus after an OS dialog
  React.useEffect(() => {
    function onWinFocus() {
      if (pickerOpenRef.current) {
        pickerOpenRef.current = false;
        lastPickedAtRef.current = Date.now();
      }
    }
    window.addEventListener('focus', onWinFocus);
    return () => window.removeEventListener('focus', onWinFocus);
  }, []);

  // Clear pending files when switching conversations
  const prevConvIdRef = React.useRef(convId);
  React.useEffect(() => {
    if (prevConvIdRef.current !== convId) {
      prevConvIdRef.current = convId;
      cancelPending();
    }
  }, [convId]);

  React.useEffect(() => { _qrLoad().catch(() => {}); }, []);

  // Fecha a lista de respostas rápidas ao clicar fora
  React.useEffect(() => {
    if (!showQuick) return;
    const h = e => { if (quickRef.current && !quickRef.current.contains(e.target)) setShowQuick(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [showQuick]);

  function openQuick() {
    setShowQuick(v => !v); setShowEmoji(false); setShowAttach(false); setQrSearch('');
    _qrLoad().then(list => setQrList(list));
  }

  // Aplica uma resposta rápida: só texto → campo de mensagem; com anexos → prévia de envio com o texto como legenda
  async function applyQuickReply(r) {
    setSlashSuggestions([]);
    setShowQuick(false);
    const media = _qrMedia(r);
    const el = textRef.current;
    if (!media.length) {
      setText(r.message || '');
      setTimeout(() => {
        if (el) { el.focus(); el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 120) + 'px'; }
      }, 30);
      return;
    }
    setQrLoading(true);
    try {
      const files = await _qrMediaFiles(r);
      const items = files.map((file, i) => {
        const mt = _qrMediaType(file.type);
        return { file, mediaType: mt, previewUrl: (mt === 'image' || mt === 'video') ? URL.createObjectURL(file) : null,
          caption: i === 0 ? (r.message || '') : '' };
      });
      if (psRef.current.list.length > 0) dispatchP({ type: 'ADD', items, caption: textRef2.current.trim() });
      else dispatchP({ type: 'INIT', items });
      setText(items[0].caption);
      setTimeout(() => { if (el) { el.style.height = 'auto'; el.focus(); } }, 30);
    } catch (e) {
      window.dispatchEvent(new CustomEvent('horos:toast', { detail: {
        module: 'system', title: 'Anexo indisponível',
        body: `Não foi possível carregar "${e.message}" da resposta /${r.shortcut}.`,
      } }));
    } finally {
      setQrLoading(false);
    }
  }

  // Handle prefill from Quick Replies (objeto da resposta ou texto)
  React.useEffect(() => {
    if (prefillText && typeof prefillText === 'object') {
      applyQuickReply(prefillText);
      onPrefillConsumed && onPrefillConsumed();
      return;
    }
    if (prefillText) {
      setText(prefillText);
      setTimeout(() => {
        const el = textRef.current;
        if (el) { el.focus(); el.style.height = 'auto'; el.style.height = Math.min(el.scrollHeight, 120) + 'px'; }
      }, 50);
      onPrefillConsumed && onPrefillConsumed();
    }
  }, [prefillText]);

  function insertAtCursor(emoji) {
    const el = textRef.current;
    if (!el) { setText(v => v + emoji); return; }
    const s = el.selectionStart, e = el.selectionEnd;
    const next = text.slice(0, s) + emoji + text.slice(e);
    setText(next);
    requestAnimationFrame(() => { el.selectionStart = el.selectionEnd = s + emoji.length; el.focus(); });
  }

  function handleKey(e) {
    if (slashSuggestions.length > 0) {
      if (e.key === 'ArrowDown') { e.preventDefault(); setSlashActive(v => Math.min(v + 1, slashSuggestions.length - 1)); return; }
      if (e.key === 'ArrowUp')   { e.preventDefault(); setSlashActive(v => Math.max(v - 1, 0)); return; }
      if (e.key === 'Tab' || (e.key === 'Enter' && !e.shiftKey)) {
        e.preventDefault();
        applySuggestion(slashSuggestions[slashActive]);
        return;
      }
      if (e.key === 'Escape') { setSlashSuggestions([]); return; }
    }
    if (e.key === 'Enter' && !e.shiftKey) {
      e.preventDefault();
      if (Date.now() - lastPickedAtRef.current < 800) return; // spurious Enter from OS file dialog
      send();
    }
  }

  function applySuggestion(reply) {
    applyQuickReply(reply);
  }

  function handleTextChange(e) {
    const val = e.target.value;
    setText(val);
    e.target.style.height = 'auto';
    e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px';
    // "/" abre as respostas rápidas salvas; o que vem depois filtra pelo atalho ou pelo texto
    if (val.startsWith('/') && !val.includes(' ')) {
      const q = val.slice(1).toLowerCase();
      const filterQr = list => list.filter(r => !q
        || r.shortcut?.toLowerCase().includes(q) || (r.message || '').toLowerCase().includes(q));
      if (_qrCache) {
        setSlashSuggestions(filterQr(_qrCache));
        setSlashActive(0);
      } else {
        _qrLoad().then(list => {
          if (textRef2.current !== val) return; // usuário já digitou outra coisa
          setSlashSuggestions(filterQr(list));
          setSlashActive(0);
        }).catch(() => setSlashSuggestions([]));
      }
    } else {
      setSlashSuggestions([]);
    }
  }

  function send() {
    if (ps.active) { sendPending(); return; }
    if (!text.trim() || disabled) return;
    onSend(text.trim());
    setText('');
    setSlashSuggestions([]);
    const el = textRef.current;
    if (el) { el.style.height = 'auto'; el.focus(); }
  }

  function openAttach(accept, mediaType) {
    pendingAccept.current = accept;
    pendingMedia.current  = mediaType;
    setShowAttach(false);
    pickerOpenRef.current = true;
    fileRef.current.accept   = accept;
    fileRef.current.multiple = (mediaType === 'image') || psRef.current.list.length > 0;
    fileRef.current.click();
  }

  function onFileChange(e) {
    const files = e.target.files;
    if (!files || files.length === 0) return;
    const items = [];
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      let mt = pendingMedia.current;
      if (file.type.startsWith('video/')) mt = 'video';
      else if (file.type.startsWith('image/')) mt = 'image';
      const url = (mt === 'image' || mt === 'video') ? URL.createObjectURL(file) : null;
      items.push({ file, mediaType: mt, previewUrl: url, caption: '' });
    }
    e.target.value = '';
    lastPickedAtRef.current = Date.now();
    if (textRef.current) textRef.current.blur();
    if (psRef.current.list.length > 0) {
      dispatchP({ type: 'ADD', items, caption: textRef2.current.trim() });
    } else {
      dispatchP({ type: 'INIT', items });
      const el = textRef.current;
      if (el) { el.style.height = 'auto'; }
    }
    setText('');
  }

  function switchPending(idx) {
    const curCaption = textRef2.current.trim();
    const { list, idx: curIdx } = psRef.current;
    const updated = list.map((item, i) => i === curIdx ? { ...item, caption: curCaption } : item);
    dispatchP({ type: 'SWITCH', idx, caption: curCaption });
    setText(updated[idx]?.caption || '');
    const el = textRef.current;
    if (el) { el.style.height = 'auto'; el.focus(); }
  }

  function removePendingItem(idx) {
    const { list } = psRef.current;
    if (list[idx]?.previewUrl) URL.revokeObjectURL(list[idx].previewUrl);
    const next = list.filter((_, i) => i !== idx);
    dispatchP({ type: 'REMOVE', idx });
    if (next.length === 0) {
      setText('');
    } else {
      const ni = idx >= next.length ? next.length - 1 : idx;
      setText(next[ni]?.caption || '');
    }
  }

  function cancelPending() {
    psRef.current.list.forEach(item => { if (item.previewUrl) URL.revokeObjectURL(item.previewUrl); });
    dispatchP({ type: 'RESET' });
    setText('');
    const el = textRef.current;
    if (el) { el.style.height = 'auto'; }
  }

  function sendPending() {
    const { active, list, idx } = psRef.current;
    if (!active || disabled) return;
    const caption = textRef2.current.trim();
    const finalList = list.length > 0
      ? list.map((item, i) => i === idx ? { ...item, caption } : item)
      : [{ ...active, caption }];
    dispatchP({ type: 'RESET' });
    setText('');
    const el = textRef.current;
    if (el) { el.style.height = 'auto'; el.focus(); }
    (async () => {
      for (const item of finalList) {
        // Áudio não aceita legenda no WhatsApp — o texto vai como mensagem logo depois
        const audioCap = item.mediaType === 'audio' && item.caption;
        await onSendMedia({ mediaType: item.mediaType, file: item.file, previewUrl: item.previewUrl, mimetype: item.file.type, fileName: item.file.name, caption: audioCap ? '' : item.caption });
        if (audioCap) onSend(item.caption);
      }
    })();
  }

  function fmtRecTime(s) {
    const m = Math.floor(s / 60);
    return `${m}:${String(s % 60).padStart(2, '0')}`;
  }

  function stopInterval() {
    if (recIntervalRef.current) { clearInterval(recIntervalRef.current); recIntervalRef.current = null; }
  }

  async function toggleRecord() {
    if (recording) return;
    try {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const mimeType = MediaRecorder.isTypeSupported('audio/webm;codecs=opus')
        ? 'audio/webm;codecs=opus'
        : MediaRecorder.isTypeSupported('audio/ogg;codecs=opus')
        ? 'audio/ogg;codecs=opus'
        : 'audio/webm';
      const rec = new MediaRecorder(stream, { mimeType });
      const ch = [];
      cancelRecRef.current = false;
      rec.ondataavailable = e => ch.push(e.data);
      rec.onstop = async () => {
        stream.getTracks().forEach(t => t.stop());
        stopInterval();
        setRec(false);
        setRecSeconds(0);
        setRecPaused(false);
        if (cancelRecRef.current) return;
        const blob = new Blob(ch, { type: mimeType });
        const ext  = mimeType.includes('ogg') ? 'ogg' : 'webm';
        const reader = new FileReader();
        reader.onload = async (ev) => {
          const base64 = ev.target.result.split(',')[1];
          await onSendMedia({ mediaType: 'audio', base64, mimetype: mimeType, fileName: `audio.${ext}`, caption: '' });
        };
        reader.readAsDataURL(blob);
      };
      rec.start();
      setMediaRec(rec);
      setChunks(ch);
      setRec(true);
      setRecSeconds(0);
      recIntervalRef.current = setInterval(() => setRecSeconds(v => v + 1), 1000);
    } catch { alert('Permissão de microfone negada'); }
  }

  function cancelRecord() {
    cancelRecRef.current = true;
    mediaRec?.stop();
  }

  function sendRecord() {
    cancelRecRef.current = false;
    mediaRec?.stop();
  }

  function togglePause() {
    if (!mediaRec) return;
    if (recPaused) {
      mediaRec.resume();
      recIntervalRef.current = setInterval(() => setRecSeconds(v => v + 1), 1000);
      setRecPaused(false);
    } else {
      mediaRec.pause();
      stopInterval();
      setRecPaused(true);
    }
  }

  const pendingCur = ps.active;

  return (
    <div className="mc-input-wrap">
      {showEmoji && (
        <_EmojiPicker onSelect={e => { insertAtCursor(e); setShowEmoji(false); }}
          onClose={() => setShowEmoji(false)}
          style={{ position: 'absolute', bottom: 'calc(100% + 6px)', left: 8, zIndex: 200 }} />
      )}

      {showAttach && (
        <div className="mc-attach-menu" style={{ position: 'absolute', bottom: 'calc(100% + 8px)', left: 14, transform: 'none', zIndex: 200 }}>
          {_ATTACH_OPTS.map(opt => (
            <button key={opt.key} className="mc-attach-menu-item"
              onClick={() => openAttach(opt.accept, opt.mediaType)}>
              <span className="mc-attach-menu-icon"><MIcon name={opt.icon} size={18} /></span>
              {opt.label}
            </button>
          ))}
        </div>
      )}

      {showQuick && (() => {
        const q = qrSearch.trim().toLowerCase().replace(/^\//, '');
        const list = (qrList || []).filter(r => !q
          || r.shortcut?.toLowerCase().includes(q) || (r.message || '').toLowerCase().includes(q));
        return (
          <div className="mc-qr-pop" ref={quickRef}>
            <div className="mc-qr-pop-head">
              <input className="mc-qr-pop-search" autoFocus placeholder="Buscar resposta rápida…" value={qrSearch}
                onChange={e => setQrSearch(e.target.value)}
                onKeyDown={e => {
                  if (e.key === 'Escape') setShowQuick(false);
                  if (e.key === 'Enter' && list[0]) { e.preventDefault(); applyQuickReply(list[0]); }
                }} />
            </div>
            <div className="mc-qr-pop-list">
              {qrList === null ? (
                <SkelList rows={3} avatar={0} pad="8px 12px" />
              ) : list.length === 0 ? (
                <div className="mc-qr-pop-empty">{q ? 'Nenhuma resposta encontrada' : 'Nenhuma resposta rápida'}</div>
              ) : list.map(r => (
                <button key={r.id} className="mc-qr-pop-item" onClick={() => applyQuickReply(r)}>
                  <span className="mc-slash-shortcut">/{r.shortcut}</span>
                  {r.message && <span className="mc-qr-pop-msg">{r.message}</span>}
                  <_QrMediaBadges media={_qrMedia(r)} />
                </button>
              ))}
            </div>
            <button className="mc-qr-pop-manage" onClick={() => { setShowQuick(false); setShowQrManage(true); }}>
              Gerenciar respostas rápidas
            </button>
          </div>
        );
      })()}

      {showQrManage && (
        <_QuickRepliesPanel onClose={() => { setShowQrManage(false); setQrList(_qrCache); }}
          onSelectReply={r => applyQuickReply(r)} />
      )}

      {replyTo && (
        <div className="mc-reply-preview">
          <div className="mc-reply-bar" />
          <div className="mc-reply-content">
            <div className="mc-reply-label">{replyTo.fromMe ? 'Você' : (contactName || 'Contato')}</div>
            <div className="mc-reply-text">{replyTo.body || '[mídia]'}</div>
          </div>
          <button className="mc-reply-close" onClick={onCancelReply}><MIcon name="xmark" size={14} /></button>
        </div>
      )}

      {pendingCur && (
        <div className="mc-pending-viewer">
          <div className="mc-pending-viewer-main">
            {pendingCur.previewUrl && pendingCur.file.type.startsWith('image/') && (
              <img src={pendingCur.previewUrl} alt="preview" className="mc-pending-viewer-img" />
            )}
            {pendingCur.previewUrl && pendingCur.file.type.startsWith('video/') && (
              <video src={pendingCur.previewUrl} controls className="mc-pending-viewer-vid" />
            )}
            {!pendingCur.previewUrl && (
              <div className="mc-pending-viewer-doc">
                <MIcon name="doc" size={32} />
                <span>{pendingCur.file.name}</span>
              </div>
            )}
            <button className="mc-pending-viewer-close" onClick={() => removePendingItem(ps.idx)} title="Remover">
              <MIcon name="xmark" size={13} />
            </button>
          </div>
          <div className="mc-pending-strip">
            {ps.list.map(function(item, i) {
              return (
                <button key={i} className={'mc-pending-pill' + (i === ps.idx ? ' active' : '')}
                  onClick={() => switchPending(i)}>
                  {item.previewUrl && item.file.type.startsWith('image/') && (
                    <img src={item.previewUrl} className="mc-pending-pill-img" alt="" />
                  )}
                  {item.previewUrl && !item.file.type.startsWith('image/') && (
                    <video src={item.previewUrl} className="mc-pending-pill-img" />
                  )}
                  {!item.previewUrl && <MIcon name="doc" size={18} />}
                </button>
              );
            })}
            {pendingCur.previewUrl && (
              <button className="mc-pending-pill mc-pending-add" onClick={() => openAttach('image/*,video/*', 'image')} title="Adicionar mais">
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
                  <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
                </svg>
              </button>
            )}
          </div>
        </div>
      )}

      <input ref={fileRef} type="file" style={{ display: 'none' }} onChange={onFileChange} />

      {slashSuggestions.length === 0 && text.startsWith('/') && !text.includes(' ') && _qrCache && (
        <div className="mc-slash-suggest">
          <div className="mc-qr-pop-empty">Nenhuma mensagem rápida encontrada</div>
        </div>
      )}
      {slashSuggestions.length > 0 && (
        <div className="mc-slash-suggest">
          {slashSuggestions.map((r, i) => (
            <button key={r.id} className={`mc-slash-item${i === slashActive ? ' active' : ''}`}
              onMouseDown={e => { e.preventDefault(); applySuggestion(r); }}>
              <span className="mc-slash-shortcut">/{r.shortcut}</span>
              <span className="mc-slash-msg">{r.message}</span>
              <_QrMediaBadges media={_qrMedia(r)} />
            </button>
          ))}
        </div>
      )}

      {/* Compose bar + audio/send (outside) */}
      <div className="mc-input-row">
        <div className="mc-compose-bar">
          {!recording && (
            /* Clip — oculto durante gravação */
            <button className={`mc-compose-icon-btn${showAttach ? ' active' : ''}`}
              title="Anexar" onClick={() => { setShowAttach(v => !v); setShowEmoji(false); }} disabled={disabled}>
              <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/>
              </svg>
            </button>
          )}

          {recording ? (
            /* ── Recording bar ── */
            <>
              <button className="mc-compose-icon-btn" title="Cancelar gravação" onClick={cancelRecord}>
                <MIcon name="trash" size={18} />
              </button>
              <div className="mc-rec-timer">
                <span className="mc-rec-live-dot" />
                <span className="mc-rec-time">{fmtRecTime(recSeconds)}</span>
              </div>
              <div className={`mc-rec-wave${recPaused ? ' paused' : ''}`}>
                {Array.from({ length: 22 }).map((_, i) => (
                  <span key={i} className="mc-rec-bar" style={{ animationDelay: `${(i * 0.07).toFixed(2)}s` }} />
                ))}
              </div>
              <button className="mc-compose-icon-btn" title={recPaused ? 'Retomar' : 'Pausar'} onClick={togglePause}>
                {recPaused ? (
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>
                ) : (
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/></svg>
                )}
              </button>
              <button className="mc-rec-send-btn" title="Enviar áudio" onClick={sendRecord}>
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M22 2L11 13M22 2L15 22l-4-9-9-4 20-7z"/>
                </svg>
              </button>
            </>
          ) : (
            /* ── Normal bar ── */
            <>
              {/* Emoji — ao lado do clip */}
              <button className={`mc-compose-icon-btn${showEmoji ? ' active' : ''}`}
                title="Emoji" onClick={() => { setShowEmoji(v => !v); setShowAttach(false); }} disabled={disabled}>
                <MIcon name="smile" size={20} />
              </button>

              {/* Respostas rápidas */}
              <button className={`mc-compose-icon-btn${showQuick ? ' active' : ''}`}
                title="Respostas rápidas (ou digite /)" onMouseDown={e => e.stopPropagation()}
                onClick={openQuick} disabled={disabled || qrLoading}>
                {qrLoading ? <span className="mc-qr-spin" /> : <MIcon name="zap" size={19} />}
              </button>

              {/* Textarea */}
              <textarea ref={textRef} className="mc-compose-ta" rows={1}
                placeholder={ps.active && ps.active.previewUrl ? 'Adicionar legenda…' : 'Mensagem ou / para mensagens rápidas'} value={text}
                onChange={handleTextChange} onKeyDown={handleKey}
                disabled={disabled} />

              {/* Ass + toggle */}
              <div className="mc-compose-ass-wrap">
                <span className="mc-compose-ass-label">Ass</span>
                <button className={`mc-compose-ass-toggle${assOn ? ' on' : ''}`}
                  title={assOn ? 'Desativar assistente IA' : 'Ativar assistente IA'}
                  onClick={() => onToggleAss && onToggleAss(!assOn)} disabled={disabled || !onToggleAss}>
                  <span className="mc-compose-ass-knob" />
                </button>
              </div>

              {/* Mic ↔ Send */}
              {(ps.active || text.trim()) ? (
                <button className="mc-compose-send-inner" onClick={send} disabled={disabled}>
                  <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
                    <path d="M22 2L11 13M22 2L15 22l-4-9-9-4 20-7z"/>
                  </svg>
                </button>
              ) : (
                <button className="mc-compose-icon-btn" title="Gravar áudio" onClick={toggleRecord} disabled={disabled}>
                  <MIcon name="mic" size={20} />
                </button>
              )}
            </>
          )}
        </div>
      </div>
    </div>
  );
});

/* ── ChatHeader ──────────────────────────────────────────────────────────── */

function _ChatHeader({ conv, lead, waLabels, onSync, panelOpen, onTogglePanel, onOpenPanel, isSyncingConv, onAction, onToggleLabel, onBack }) {
  const phone = conv ? (typeof fmtPhone === 'function' ? fmtPhone(conv.phone) : conv.phone) : '';
  const tagLabels    = _waTagLabels(waLabels);
  const convLabels   = tagLabels.filter(l => (conv?.labels || []).includes(l.label_id));
  const addableLabels = tagLabels.filter(l => !(conv?.labels || []).includes(l.label_id));
  // "+ Tag": lista das etiquetas que a conversa ainda não tem; fecha ao clicar fora ou trocar de conversa
  const [tagPicker, setTagPicker] = React.useState(false);
  const tagRowRef = React.useRef(null);
  React.useEffect(() => { setTagPicker(false); }, [conv?.id]);
  React.useEffect(() => {
    if (!tagPicker) return;
    const h = e => { if (tagRowRef.current && !tagRowRef.current.contains(e.target)) setTagPicker(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [tagPicker]);
  const tempLabel = lead?.temp === 'hot' ? 'Lead quente' : lead?.temp === 'warm' ? 'Lead morno' : lead?.temp === 'cold' ? 'Lead frio' : null;
  const tempStyle = lead?.temp === 'hot'
    ? { background: 'rgba(255,90,78,0.12)', color: '#ff5a4e', borderColor: 'rgba(255,90,78,0.22)' }
    : lead?.temp === 'warm'
    ? { background: 'rgba(245,181,68,0.12)', color: '#f5b544', borderColor: 'rgba(245,181,68,0.25)' }
    : { background: 'rgba(89,200,255,0.1)', color: '#59c8ff', borderColor: 'rgba(89,200,255,0.25)' };

  const CallBtn = window.LeadCallButton;

  const actionBtn = (icon, title, disabled) => (
    <button key={title} className="mc-head-action-btn" title={title} disabled={disabled}
      style={disabled ? { opacity: 0.4, cursor: 'default' } : {}}>
      <Icon name={icon} size={14} />
    </button>
  );

  return (
    <div className="mc-chat-head">
      <div className="mc-chat-head-main">
        {/* Celular: volta para a lista de conversas */}
        {onBack && (
          <button type="button" className="mc-chat-back" aria-label="Voltar para as conversas" onClick={onBack}>
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m15 18-6-6 6-6"/></svg>
          </button>
        )}
        {/* Clique na foto/nome/telefone: lead → ficha completa; contato sem lead → painel direito */}
        <div className="mc-chat-head-left mc-chat-head-profile" role="button" tabIndex={0}
          title={lead ? 'Abrir ficha do cliente' : 'Dados do contato'}
          onClick={() => onOpenPanel?.()}
          onKeyDown={e => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onOpenPanel?.(); } }}>
          <MsgAvatar name={conv?.name || '?'} pic={conv?.pic} size={36} phone={conv?.phone} />
          <div className="mc-chat-head-meta">
            <div className="mc-chat-head-name">
              <h2>{conv?.name || 'Conversa'}</h2>
              {tempLabel && <span className="mc-tag" style={{ fontSize: 10, ...tempStyle }}>{tempLabel}</span>}
            </div>
            {conv?.typing ? (
              <div className="mc-chat-head-presence" style={{ color: '#34c759', fontWeight: 500 }}>
                <div className="mc-typing-dots">
                  <span /><span /><span />
                </div>
                <span style={{ marginLeft: 4 }}>digitando…</span>
              </div>
            ) : isSyncingConv ? (
              <div className="mc-chat-head-presence" style={{ color: 'var(--fg-4)' }}>
                <div style={{ width: 9, height: 9, border: '1.5px solid var(--fg-4)', borderTopColor: 'var(--accent)', borderRadius: '50%', animation: 'spin 0.7s linear infinite', flexShrink: 0 }} />
                <span style={{ marginLeft: 5 }}>sincronizando…</span>
              </div>
            ) : phone ? (
              <div className="mc-chat-head-presence">
                <span style={{ fontFamily: 'var(--mono)', fontSize: 11.5 }}>{phone}</span>
                <span className="dot-sep" />
                <span>WhatsApp</span>
              </div>
            ) : null}
          </div>
        </div>
        <div className="mc-chat-head-actions">
          {conv?.phone && CallBtn && <CallBtn lead={{ id: lead?.id, phone: conv.phone }} />}
          <button className="mc-head-action-btn" title="Painel direito" onClick={onTogglePanel}
            style={panelOpen ? { background: 'rgba(31,147,255,0.1)', color: 'var(--accent)', borderColor: 'rgba(31,147,255,0.3)' } : {}}>
            <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
              <rect x="1.8" y="2.8" width="12.4" height="10.4" rx="1.6"/>
              <path d="M10 2.8v10.4"/>
            </svg>
          </button>
          <button className="icon-btn" title="Sincronizar com o WhatsApp" onClick={onSync}>
            <Icon name="refresh" size={16} style={isSyncingConv ? { animation: 'spin 0.8s linear infinite' } : {}} />
          </button>
        </div>
      </div>
      {/* Etiquetas — em TODA conversa: × remove, "+ Tag" adiciona (grava no WhatsApp via toggleLabel) */}
      {tagLabels.length > 0 && (
        <div className="mc-head-tags" ref={tagRowRef}>
          {convLabels.map(l => (
            <span key={l.label_id} className="mc-head-tag"
              style={{ color: _waLabelColor(l.color), borderColor: `${_waLabelColor(l.color)}40`, background: `${_waLabelColor(l.color)}10` }}>
              {l.name}
              {onToggleLabel && (
                <button className="mc-head-tag-x" title={`Remover ${l.name}`}
                  onClick={() => onToggleLabel(l.label_id, true)}>
                  <MIcon name="xmark" size={8} />
                </button>
              )}
            </span>
          ))}
          <button className="mc-head-tag-add"
            disabled={!onToggleLabel || addableLabels.length === 0}
            title={!onToggleLabel ? 'Conecte o WhatsApp para editar etiquetas'
              : addableLabels.length === 0 ? 'A conversa já tem todas as etiquetas' : 'Adicionar etiqueta'}
            onClick={() => setTagPicker(v => !v)}>
            + Tag
          </button>
          {tagPicker && (
            <div className="mc-rp2-label-picker mc-head-tag-picker">
              {addableLabels.map(l => (
                <button key={l.label_id} className="mc-rp2-label-pick-item"
                  onClick={() => { onToggleLabel(l.label_id, false); setTagPicker(false); }}>
                  <span style={{ width: 8, height: 8, borderRadius: '50%', background: _waLabelColor(l.color), flexShrink: 0 }} />
                  <span style={{ color: 'var(--fg)' }}>{l.name}</span>
                </button>
              ))}
            </div>
          )}
        </div>
      )}
    </div>
  );
}

/* ── ConnectScreen ───────────────────────────────────────────────────────── */

function _WaChoiceModal({ onClose, onConnectWeb }) {
  React.useEffect(() => {
    function h(e) { if (e.key === 'Escape') onClose(); }
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, []);

  const cardStyle = {
    flex: 1, background: 'var(--card-2)', borderRadius: 16, padding: '28px 24px',
    display: 'flex', flexDirection: 'column', gap: 18,
    border: '1px solid var(--line)',
  };
  const iconWrap = (bg) => ({
    width: 44, height: 44, borderRadius: 12, background: bg,
    display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
  });
  const bullet = (label) => (
    <div key={label} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, color: 'var(--fg-2)' }}>
      <svg width="16" height="16" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
        <circle cx="8" cy="8" r="8" fill="rgba(31,147,255,0.15)" />
        <path d="M5 8l2 2 4-4" stroke="var(--accent)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
      </svg>
      {label}
    </div>
  );
  const btnBase = {
    width: '100%', border: 'none', borderRadius: 10, padding: '13px 16px',
    fontSize: 14, fontWeight: 600, fontFamily: 'inherit', cursor: 'pointer',
    display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
    marginTop: 'auto', transition: 'opacity 150ms',
  };

  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 9000,
      background: 'rgba(0,0,0,0.55)', backdropFilter: 'blur(6px)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
    }} onClick={onClose}>
      <div style={{
        background: 'var(--card)', borderRadius: 20, padding: 28, maxWidth: 620, width: 'calc(100% - 48px)',
        boxShadow: '0 24px 64px rgba(0,0,0,0.4)', display: 'flex', flexDirection: 'column', gap: 20,
        border: '1px solid var(--line-strong)',
      }} onClick={e => e.stopPropagation()}>

        {/* Header */}
        <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between' }}>
          <div>
            <div style={{ fontSize: 17, fontWeight: 700, color: 'var(--fg)', marginBottom: 4 }}>Conectar WhatsApp</div>
            <div style={{ fontSize: 13, color: 'var(--fg-3)' }}>Escolha como integrar o WhatsApp ao Horos</div>
          </div>
          <button onClick={onClose} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--fg-4)', padding: 4, lineHeight: 1 }}>
            <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
              <path d="M3 3l12 12M15 3L3 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
            </svg>
          </button>
        </div>

        {/* Cards */}
        <div style={{ display: 'flex', gap: 16 }}>
          {/* Card 1 — WhatsApp Web */}
          <div style={cardStyle}>
            <div style={iconWrap('rgba(31,147,255,0.12)')}>
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none">
                <rect x="3" y="3" width="8" height="8" rx="1.5" stroke="var(--accent)" strokeWidth="1.8"/>
                <rect x="13" y="3" width="8" height="8" rx="1.5" stroke="var(--accent)" strokeWidth="1.8"/>
                <rect x="3" y="13" width="8" height="8" rx="1.5" stroke="var(--accent)" strokeWidth="1.8"/>
                <rect x="15" y="15" width="2" height="2" fill="var(--accent)"/>
                <rect x="19" y="15" width="2" height="2" fill="var(--accent)"/>
                <rect x="15" y="19" width="2" height="2" fill="var(--accent)"/>
                <rect x="19" y="19" width="2" height="2" fill="var(--accent)"/>
              </svg>
            </div>
            <div>
              <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--fg)', marginBottom: 3 }}>WhatsApp Web</div>
              <div style={{ fontSize: 12.5, color: 'var(--fg-3)' }}>Sem precisar da API oficial</div>
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              {['Acesso pelo celular e computador', 'QR Code escaneado pelo app', 'Configuração em segundos'].map(bullet)}
            </div>
            <button style={{ ...btnBase, background: 'var(--accent)', color: '#fff' }} onClick={() => { onClose(); onConnectWeb(); }}>
              <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
                <path d="M2 7h10M8 3l4 4-4 4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
              Conectar via QR code
            </button>
          </div>

          {/* Card 2 — API Oficial */}
          <div style={{ ...cardStyle, opacity: 0.6 }}>
            <div style={iconWrap('rgba(124,58,237,0.12)')}>
              <svg width="22" height="22" viewBox="0 0 24 24" fill="none">
                <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" stroke="#7c3aed" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
            </div>
            <div>
              <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--fg)', marginBottom: 3 }}>API Oficial Meta</div>
              <div style={{ fontSize: 12.5, color: 'var(--fg-3)' }}>Conexão direta com a Meta</div>
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              {['Mais estável que QR Code', 'Suporte a templates (HSM)', 'Não depende do celular'].map(bullet)}
            </div>
            <button style={{ ...btnBase, background: 'rgba(124,58,237,0.5)', color: '#fff', cursor: 'default' }} disabled>
              <svg width="14" height="14" viewBox="0 0 14 14" fill="none">
                <path d="M2 7h10M8 3l4 4-4 4" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
              </svg>
              Indisponível
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

function _WaBanBanner({ info, onDismiss }) {
  const isHardBan = info?.type === 'wa_banned' && info?.payload?.statusReason === 403;
  const isWarning = info?.type === 'wa_ban_warning';
  const color     = isHardBan ? '#ff5a4e' : '#f5b544';
  const icon      = isHardBan ? 'ban' : 'alert';
  return (
    <div style={{
      background: `${color}11`,
      borderBottom: `1px solid ${color}38`,
      padding: '10px 20px',
      display: 'flex', alignItems: 'flex-start', gap: 12,
      fontFamily: 'Inter,sans-serif', flexShrink: 0,
    }}>
      <span style={{ lineHeight: '1.4', flexShrink: 0, color }}><MIcon name={icon} size={18} /></span>
      <div style={{ flex: 1 }}>
        <div style={{ fontWeight: 700, fontSize: 13, color: 'var(--fg)', marginBottom: 2 }}>
          {info.title}
        </div>
        <div style={{ fontSize: 12, color: 'var(--fg-3)', lineHeight: 1.5 }}>
          {info.body}
        </div>
        {isWarning && (
          <div style={{ fontSize: 11, color: color, marginTop: 4, fontWeight: 600 }}>
            Dica: evite enviar muitas mensagens para contatos que não têm seu número salvo.
          </div>
        )}
      </div>
      <button onClick={onDismiss} style={{
        background: 'none', border: 'none', cursor: 'pointer',
        color: 'var(--fg-4)', fontSize: 18, lineHeight: 1,
        padding: '0 4px', flexShrink: 0,
      }}><MIcon name="xmark" size={16} /></button>
    </div>
  );
}

function _ConnectScreen({ phase, qr, onConnect, onDisconnect, onRefreshQr, waPhone }) {
  const [showModal, setShowModal] = React.useState(false);
  const [timedOut, setTimedOut]   = React.useState(false);
  const [countdown, setCountdown] = React.useState(20);
  const [refreshing, setRefreshing] = React.useState(false);
  const timeoutRef = React.useRef(null);

  // Se ficou >= 45s em 'connecting'/'qr' sem QR, mostra erro
  React.useEffect(() => {
    if ((phase === 'connecting' || phase === 'qr') && !qr) {
      timeoutRef.current = setTimeout(() => setTimedOut(true), 45000);
    } else {
      clearTimeout(timeoutRef.current);
      setTimedOut(false);
    }
    return () => clearTimeout(timeoutRef.current);
  }, [phase, qr]);

  // Countdown de expiração do QR — reinicia a cada novo QR recebido
  React.useEffect(() => {
    if (!qr) return;
    setCountdown(20);
    const t = setInterval(() => setCountdown(c => Math.max(0, c - 1)), 1000);
    return () => clearInterval(t);
  }, [qr]);

  async function handleRefresh() {
    if (!onRefreshQr || refreshing) return;
    setRefreshing(true);
    await onRefreshQr();
    setTimeout(() => setRefreshing(false), 1500);
  }

  if (phase === 'connected') return null;

  const center = {
    height: '100%', display: 'flex', flexDirection: 'column',
    alignItems: 'center', justifyContent: 'center',
  };

  if (phase === 'loading') return (
    <div style={{ ...center, gap: 14, color: 'var(--fg-4)' }}>
      <div style={{ width: 28, height: 28, border: '2.5px solid var(--border)', borderTopColor: 'var(--accent)', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }} />
      <span style={{ fontSize: 14, fontFamily: 'Inter,sans-serif' }}>Verificando conexão…</span>
    </div>
  );

  if (phase === 'idle') return (
    <div style={{ ...center, gap: 24 }}>
      <div style={{ textAlign: 'center', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
        <div style={{
          width: 72, height: 72, borderRadius: '50%',
          background: 'rgba(31,147,255,0.08)',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          <svg width="34" height="34" viewBox="0 0 24 24" fill="none">
            <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z" fill="var(--accent)"/>
            <path d="M20.52 3.449C18.24 1.245 15.24 0 12.045 0 5.463 0 .104 5.334.101 11.893c0 2.096.549 4.14 1.595 5.945L0 24l6.335-1.652C8.073 23.267 10.035 23.73 12 23.73h.007c6.578 0 11.934-5.335 11.937-11.892.003-3.181-1.242-6.17-3.424-8.389zm-8.471 18.297h-.006c-1.773 0-3.513-.474-5.031-1.37l-.361-.214-3.741.976.999-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.002-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.892 6.993c-.002 5.45-4.436 9.884-9.883 9.884z" fill="var(--accent)"/>
          </svg>
        </div>
        <div style={{ fontSize: 16, fontWeight: 700, color: 'var(--fg)', fontFamily: 'Inter,sans-serif' }}>
          WhatsApp não conectado
        </div>
        <div style={{ fontSize: 13, color: 'var(--fg-3)', fontFamily: 'Inter,sans-serif' }}>
          Conecte seu WhatsApp para ver as conversas
        </div>
      </div>
      <button onClick={() => setShowModal(true)} style={{
        background: '#1F93FF', color: '#fff', border: 'none', borderRadius: 10,
        padding: '11px 28px', fontSize: 14, fontWeight: 600, fontFamily: 'Inter,sans-serif',
        cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 8,
        boxShadow: '0 4px 14px rgba(31,147,255,0.35)', transition: 'opacity 150ms',
      }}
        onMouseEnter={e => e.currentTarget.style.opacity = '0.88'}
        onMouseLeave={e => e.currentTarget.style.opacity = '1'}>
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
          <path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z" fill="#fff"/>
          <path d="M20.52 3.449C18.24 1.245 15.24 0 12.045 0 5.463 0 .104 5.334.101 11.893c0 2.096.549 4.14 1.595 5.945L0 24l6.335-1.652C8.073 23.267 10.035 23.73 12 23.73h.007c6.578 0 11.934-5.335 11.937-11.892.003-3.181-1.242-6.17-3.424-8.389zm-8.471 18.297h-.006c-1.773 0-3.513-.474-5.031-1.37l-.361-.214-3.741.976.999-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.002-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.892 6.993c-.002 5.45-4.436 9.884-9.883 9.884z" fill="#fff"/>
        </svg>
        Conectar WhatsApp
      </button>
      {showModal && (
        <_WaChoiceModal onClose={() => setShowModal(false)} onConnectWeb={onConnect} />
      )}
    </div>
  );

  // QR / connecting
  const btnStyle = (primary) => ({
    background: primary ? '#1F93FF' : 'transparent',
    border: primary ? 'none' : '1px solid var(--border)',
    color: primary ? '#fff' : 'var(--fg-3)',
    borderRadius: 8, padding: '8px 20px', fontSize: 13, fontFamily: 'Inter,sans-serif',
    cursor: 'pointer',
  });

  if (timedOut) return (
    <div style={{ ...center, gap: 16, textAlign: 'center' }}>
      <div style={{ color: '#f5b544' }}><MIcon name="alert" size={36} /></div>
      <div style={{ fontSize: 15, fontWeight: 600, color: 'var(--fg)', fontFamily: 'Inter,sans-serif' }}>
        Não foi possível gerar o QR code
      </div>
      <div style={{ fontSize: 13, color: 'var(--fg-3)', maxWidth: 320, fontFamily: 'Inter,sans-serif', lineHeight: 1.5 }}>
        O servidor WhatsApp pode estar reiniciando. Aguarde alguns segundos e tente novamente.
      </div>
      <div style={{ display: 'flex', gap: 10, marginTop: 4 }}>
        <button style={btnStyle(true)} onClick={() => { setTimedOut(false); onDisconnect(); setTimeout(onConnect, 400); }}>
          Tentar novamente
        </button>
        <button style={btnStyle(false)} onClick={onDisconnect}>Cancelar</button>
      </div>
    </div>
  );

  return (
    <div style={{ ...center, gap: 14, textAlign: 'center' }}>
      <div style={{ fontSize: 15, fontWeight: 600, color: 'var(--fg)', fontFamily: 'Inter,sans-serif' }}>Escaneie o QR code</div>
      <div style={{ fontSize: 12, color: 'var(--fg-3)', marginTop: -4, fontFamily: 'Inter,sans-serif' }}>Abra o WhatsApp → Dispositivos conectados → Conectar dispositivo</div>
      {qr ? (
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 8 }}>
          <div style={{ background: '#fff', borderRadius: 12, padding: 12, display: 'inline-block', boxShadow: '0 4px 24px rgba(0,0,0,0.15)' }}>
            <img src={qr.startsWith('data:') ? qr : `data:image/png;base64,${qr}`}
              alt="QR Code" style={{ width: 264, height: 264, display: 'block' }} />
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <span style={{ fontSize: 12, color: countdown <= 5 ? '#f5b544' : 'var(--fg-4)', fontFamily: 'Inter,sans-serif' }}>
              Expira em {countdown}s
            </span>
            <button onClick={handleRefresh} disabled={refreshing} style={{
              background: 'transparent', border: '1px solid var(--border)', color: 'var(--fg-3)',
              borderRadius: 6, padding: '4px 10px', fontSize: 12, fontFamily: 'Inter,sans-serif',
              cursor: refreshing ? 'default' : 'pointer', opacity: refreshing ? 0.5 : 1,
              display: 'flex', alignItems: 'center', gap: 5,
            }}>
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <path d="M23 4v6h-6M1 20v-6h6"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
              </svg>
              Novo QR
            </button>
          </div>
        </div>
      ) : (
        <div style={{ width: 288, height: 288, background: 'var(--bg-2)', border: '1px solid var(--border)', borderRadius: 12, display: 'grid', placeItems: 'center', color: 'var(--fg-4)' }}>
          <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10 }}>
            <div style={{ width: 22, height: 22, border: '2px solid var(--border)', borderTopColor: '#1F93FF', borderRadius: '50%', animation: 'spin 0.7s linear infinite' }} />
            <span style={{ fontSize: 12, fontFamily: 'Inter,sans-serif' }}>Gerando QR…</span>
          </div>
        </div>
      )}
      <button style={btnStyle(false)} onClick={onDisconnect}>Cancelar</button>
    </div>
  );
}

/* ── ConvItem ────────────────────────────────────────────────────────────── */

const _TEMP_COLOR = { hot: '#ff5a4e', warm: '#f5b544', cold: '#59c8ff' };

// Set module-level atualizado de forma SÍNCRONA em selectConv — não depende de React state/batching.
// Garante que ao clicar em outra conversa o badge desaparece imediatamente, sem race condition.
const _CLEARED_CONVS = new Set();

function _ConvItem({ conv, selected, leadTemp, lead, waLabels, onClick, onPin, onArchive, onMarkUnread, onResolve, readConvIds, selectMode, checked, onLidLink }) {
  const isUnread    = conv.unread > 0 && !selected && !_CLEARED_CONVS.has(conv.id) && !(readConvIds && readConvIds.has(conv.id));
  // Ordem do WhatsApp (catálogo), só etiquetas atribuíveis
  const convTagIds = _waTagLabels(waLabels)
    .filter(l => (conv.labels || []).includes(l.label_id))
    .map(l => l.label_id);
  const visibleLabels = convTagIds.slice(0, 4);
  const tempColor = leadTemp ? _TEMP_COLOR[leadTemp] : null;
  const [ctxMenu, setCtxMenu] = React.useState(null);
  const [hovering, setHovering] = React.useState(false);

  React.useEffect(() => {
    if (!ctxMenu) return;
    function h(e) { setCtxMenu(null); }
    document.addEventListener('click', h);
    document.addEventListener('contextmenu', h);
    return () => { document.removeEventListener('click', h); document.removeEventListener('contextmenu', h); };
  }, [ctxMenu]);

  // Número oculto pelo WhatsApp (@lid sem número): atalho para informar o número no painel
  const hiddenNumber = !!onLidLink && !lead && !conv.phone && String(conv.jid || '').endsWith('@lid');
  const showFooter = visibleLabels.length > 0 || (hiddenNumber && !selectMode);

  return (
    <>
    <div className="mc-item-container" onMouseEnter={() => setHovering(true)} onMouseLeave={() => setHovering(false)}>
    <button className={`mc-item${selected && !selectMode ? ' sel' : ''}${isUnread ? ' unread' : ''}${checked ? ' checked' : ''}`} onClick={onClick}
      onContextMenu={e => { if (selectMode) return; e.preventDefault(); e.stopPropagation(); setCtxMenu({ x: e.clientX, y: e.clientY }); }}>
      {selectMode && (
        <span className={`mc-sel-check${checked ? ' on' : ''}`} aria-hidden="true">
          {checked && <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3.4" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>}
        </span>
      )}
      <div className="mc-avatar-wrap">
        <MsgAvatar name={conv.name} pic={conv.pic} size={40} phone={conv.phone} />
      </div>
      <div className="mc-item-body">
        <div className="mc-item-row">
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, minWidth: 0, flex: 1 }}>
            <span className="mc-item-name">{conv.name}</span>
            {tempColor && (
              <span className="mc-temp-dot" style={{ background: tempColor }} title={leadTemp === 'hot' ? 'Quente' : leadTemp === 'warm' ? 'Morno' : 'Frio'} />
            )}
          </div>
          <span className="mc-item-time">{_fmtTs(conv.ts)}</span>
        </div>
        <div className="mc-item-row" style={{ marginTop: 5 }}>
          <div className="mc-item-last">
            {conv.last ? (conv.lastFromMe ? <><span className="mc-item-last-you">Você:</span> {conv.last}</> : conv.last) : ''}
          </div>
          {isUnread && <span className="mc-unread">{conv.unread > 99 ? '99+' : conv.unread}</span>}
        </div>
      </div>
    </button>

    {showFooter && (
      <div className={`mc-item-footer${selected && !selectMode ? ' sel' : ''}${checked ? ' checked' : ''}`} onClick={onClick}>
        <div className="mc-item-tags">
          {visibleLabels.map(id => {
            const lbl = waLabels.find(l => l.label_id === id);
            if (!lbl) return null;
            return (
              <span key={id} className="mc-item-tag-chip"
                style={{ color: _waLabelColor(lbl.color), borderColor: `${_waLabelColor(lbl.color)}40`, background: `${_waLabelColor(lbl.color)}12` }}>
                {lbl.name}
              </span>
            );
          })}
          {convTagIds.length > 4 && <span className="mc-item-label-more" title={convTagIds.slice(4).map(id => waLabels.find(l => l.label_id === id)?.name).join(', ')}>+{convTagIds.length - 4}</span>}
          {hiddenNumber && !selectMode && (
            <button type="button" className="mc-item-lid-link" title="O WhatsApp escondeu o número deste contato. Informe o número para ver o nome e ligar ao lead."
              onClick={e => { e.stopPropagation(); onLidLink(conv.id); }}>
              Número oculto · Informar
            </button>
          )}
        </div>
      </div>
    )}
    </div>
    {ctxMenu && (
      <div className="mc-conv-context-menu" style={{ top: ctxMenu.y, left: ctxMenu.x }}
        onClick={e => e.stopPropagation()}>
        <button className="mc-conv-ctx-item" onClick={() => { onPin && onPin(conv.id, !conv.pinned); setCtxMenu(null); }}>
          {conv.pinned ? <><MIcon name="pin" size={13} style={{marginRight:6}} />Desafixar</> : <><MIcon name="pin" size={13} style={{marginRight:6}} />Fixar</>}
        </button>
        <button className="mc-conv-ctx-item" onClick={() => { onMarkUnread && onMarkUnread(conv.id, !isUnread); setCtxMenu(null); }}>
          {isUnread ? <><MIcon name="check" size={13} style={{marginRight:6}} />Marcar como lida</> : '● Marcar como não lida'}
        </button>
        <div className="mc-conv-ctx-sep" />
        <button className="mc-conv-ctx-item danger" onClick={() => { onArchive && onArchive(conv.id, !conv.archived); setCtxMenu(null); }}>
          <MIcon name="archive" size={13} style={{marginRight:4}} /> {conv.archived ? 'Desarquivar' : 'Arquivar'}
        </button>
      </div>
    )}
    </>
  );
}

/* ── NewConvModal — estilo "Novo contato" do WhatsApp ────────────────────── */

const NC_COUNTRIES = [
  { code: 'BR', ddi: '55',  label: 'Brasil',    min: 10, max: 11, ph: '(11) 99999-9999' },
  { code: 'US', ddi: '1',   label: 'EUA',       min: 10, max: 10, ph: '(555) 555-5555'  },
  { code: 'PT', ddi: '351', label: 'Portugal',  min: 9,  max: 9,  ph: '912 345 678'     },
  { code: 'AR', ddi: '54',  label: 'Argentina', min: 10, max: 11, ph: '11 1234-5678'    },
  { code: 'ES', ddi: '34',  label: 'Espanha',   min: 9,  max: 9,  ph: '612 345 678'     },
  { code: 'MX', ddi: '52',  label: 'México',    min: 10, max: 10, ph: '55 1234 5678'    },
];
function _ncFlag(code) {
  return 'https://flagcdn.com/w20/' + code.toLowerCase() + '.png';
}

function _ncMask(digits, code) {
  if (code === 'BR') {
    const d = digits.slice(0, 11);
    if (d.length === 0) return '';
    if (d.length <= 2) return '(' + d;
    if (d.length <= 6) return '(' + d.slice(0,2) + ') ' + d.slice(2);
    if (d.length <= 10) return '(' + d.slice(0,2) + ') ' + d.slice(2,6) + '-' + d.slice(6);
    return '(' + d.slice(0,2) + ') ' + d.slice(2,7) + '-' + d.slice(7);
  }
  if (code === 'US') {
    const d = digits.slice(0, 10);
    if (d.length === 0) return '';
    if (d.length <= 3) return '(' + d;
    if (d.length <= 6) return '(' + d.slice(0,3) + ') ' + d.slice(3);
    return '(' + d.slice(0,3) + ') ' + d.slice(3,6) + '-' + d.slice(6);
  }
  // Generic: just digits, limit to max
  const c = NC_COUNTRIES.find(x => x.code === code);
  return digits.slice(0, c ? c.max : 15);
}

function _NewConvModal({ onClose, onCreate, initialPhone }) {
  const [nome, setNome]             = React.useState('');
  const [sobrenome, setSobrenome]   = React.useState('');
  const [country, setCountry]       = React.useState(NC_COUNTRIES[0]);
  const [phone, setPhone]           = React.useState(() => initialPhone ? _ncMask(initialPhone.replace(/\D/g,''), 'BR') : '');
  const [showDDI, setShowDDI]       = React.useState(false);
  const [err, setErr]               = React.useState('');
  const [busy, setBusy]             = React.useState(false);
  const nomeRef  = React.useRef();
  const ddiRef   = React.useRef();

  React.useEffect(() => { setTimeout(() => nomeRef.current?.focus(), 80); }, []);
  React.useEffect(() => {
    function h(e) {
      if (e.key === 'Escape') { if (showDDI) { setShowDDI(false); } else { onClose(); } }
    }
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, [showDDI]);
  React.useEffect(() => {
    if (!showDDI) return;
    function h(e) { if (ddiRef.current && !ddiRef.current.contains(e.target)) setShowDDI(false); }
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [showDDI]);

  const digits   = phone.replace(/\D/g, '');
  const fullName = [nome.trim(), sobrenome.trim()].filter(Boolean).join(' ');
  const canSave  = nome.trim().length > 0 && digits.length >= country.min;

  function handlePhoneChange(e) {
    const raw = e.target.value.replace(/\D/g, '').slice(0, country.max);
    setPhone(_ncMask(raw, country.code));
  }

  function selectCountry(c) { setCountry(c); setPhone(''); setShowDDI(false); }

  async function save() {
    if (!canSave || busy) return;
    setBusy(true); setErr('');
    try { await onCreate({ name: fullName, phone: digits, ddi: country.ddi }); }
    catch (e) { setErr('Ocorreu um erro ao criar o contato. Tente novamente.'); setBusy(false); return; }
    setBusy(false);
  }

  function handlePhoneKey(e) { if (e.key === 'Enter' && canSave) save(); }

  return (
    <div className="mc-nc-panel">
      {/* Header */}
      <div className="mc-nc-head">
        <button className="mc-nc-back" onClick={onClose} title="Voltar">
          <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M19 12H5M12 5l-7 7 7 7"/>
          </svg>
        </button>
        <span className="mc-nc-title">Novo contato</span>
        <button className={`mc-nc-save${canSave ? ' ready' : ''}`} onClick={save} disabled={!canSave || busy} title="Salvar">
          {busy
            ? <div className="mc-nc-spinner" />
            : (
              <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
                <path d="M20 6L9 17l-5-5"/>
              </svg>
            )
          }
        </button>
      </div>

      {/* Avatar placeholder */}
      <div className="mc-nc-avatar-wrap">
        <div className="mc-nc-avatar">
          <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" style={{ opacity: 0.5 }}>
            <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
          </svg>
        </div>
      </div>

      {/* Fields */}
      <div className="mc-nc-fields">
        {/* Nome */}
        <div className="mc-nc-field">
          <span className="mc-nc-field-icon">
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
              <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
            </svg>
          </span>
          <input ref={nomeRef} className="mc-nc-input" placeholder="Nome" value={nome}
            onChange={e => setNome(e.target.value)} autoComplete="given-name" />
        </div>

        {/* Sobrenome */}
        <div className="mc-nc-field mc-nc-field-indent">
          <input className="mc-nc-input" placeholder="Sobrenome" value={sobrenome}
            onChange={e => setSobrenome(e.target.value)} autoComplete="family-name" />
        </div>

        {/* Telefone */}
        <div className="mc-nc-field mc-nc-field-phone">
          <span className="mc-nc-field-icon">
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
              <path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12 19.79 19.79 0 0 1 1.58 3.4 2 2 0 0 1 3.55 1.2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L7.91 8.72a16 16 0 0 0 6 6l.88-.88a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7a2 2 0 0 1 1.72 2.03z"/>
            </svg>
          </span>
          <div className="mc-nc-phone-row">
            {/* DDI picker */}
            <div className="mc-nc-ddi-wrap" ref={ddiRef}>
              <button className={`mc-nc-ddi-btn${showDDI ? ' open' : ''}`} onClick={() => setShowDDI(v => !v)} type="button">
                +{country.ddi}
                <svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
                  <path d="m2 4 4 4 4-4"/>
                </svg>
              </button>
              {showDDI && (
                <div className="mc-nc-ddi-list">
                  {NC_COUNTRIES.map(c => (
                    <button key={c.code} type="button"
                      className={`mc-nc-ddi-opt${c.code === country.code ? ' active' : ''}`}
                      onClick={() => selectCountry(c)}>
                      <img className="ddi-flag" src={_ncFlag(c.code)} width="20" height="14" alt={c.code} style={{borderRadius:2,objectFit:'cover',flexShrink:0}} />
                      <span className="ddi-name">{c.label}</span>
                      <span className="ddi-code">+{c.ddi}</span>
                    </button>
                  ))}
                </div>
              )}
            </div>
            <input className="mc-nc-input mc-nc-phone-input" placeholder={country.ph}
              value={phone} onChange={handlePhoneChange}
              onKeyDown={handlePhoneKey} inputMode="tel" autoComplete="tel" />
          </div>
        </div>
      </div>

      {err && <div className="mc-nc-error">{err}</div>}

      {/* Botão salvar */}
      <div className="mc-nc-footer">
        <button className="mc-nc-submit" onClick={save} disabled={!canSave || busy}>
          {busy
            ? <div className="mc-nc-spinner" style={{ width:16, height:16 }} />
            : 'Salvar contato'}
        </button>
      </div>
    </div>
  );
}

/* ── QuickActionModal ────────────────────────────────────────────────────── */

const _QA_CFG = {
  nota:     { label: 'Nota',     icon: 'pencil',  accent: '#6366f1', needsText: true },
  proposta: { label: 'Proposta', icon: 'message', accent: '#1F93FF', needsTitle: true, needsText: true },
  tarefa:   { label: 'Tarefa',   icon: 'check',   accent: '#f59e0b', needsTitle: true, needsDate: true, needsPrio: true },
  reuniao:  { label: 'Agenda',   icon: 'cal',     accent: '#10b981', needsTitle: true, needsDate: true, needsTime: true, needsText: true },
  lembrete: { label: 'Lembrete', icon: 'bell',    accent: '#ef4444', needsText: true,  needsDate: true, needsTime: true },
};

function _QuickActionModal({ type, conv, lead, onClose }) {
  const cfg = _QA_CFG[type] || _QA_CFG.nota;
  const [title,    setTitle]    = React.useState('');
  const [text,     setText]     = React.useState('');
  const [date,     setDate]     = React.useState('');
  const [time,     setTime]     = React.useState('');
  const [priority, setPriority] = React.useState('normal');
  const [saving,   setSaving]   = React.useState(false);
  const [saved,    setSaved]    = React.useState(false);
  const [err,      setErr]      = React.useState('');

  React.useEffect(() => {
    function h(e) { if (e.key === 'Escape') onClose(); }
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, []);

  const isValid = cfg.needsTitle ? title.trim().length > 0 : text.trim().length > 0;

  function toISO(dateStr, timeStr, plusHours = 0) {
    const base = dateStr
      ? new Date(`${dateStr}T${timeStr || '09:00'}:00`)
      : new Date(Date.now() + 3600000);
    if (plusHours) base.setHours(base.getHours() + plusHours);
    return base.toISOString();
  }

  async function handleSave() {
    if (!isValid || saving) return;
    setSaving(true); setErr('');
    try {
      const sb  = window.HOROS_SB;
      const uid = window.HOROS_USER?.id;
      const wsId = window.HOROS_WORKSPACE?.workspace_id || null;
      const phone = (conv?.phone || '').replace(/\D/g, '') || null;

      if (type === 'nota' || type === 'proposta') {
        if (!lead?.id) { setErr('Adicione este contato como lead primeiro para salvar notas.'); setSaving(false); return; }
        const note = { id: Date.now().toString(), type, title: title || null, text, createdAt: new Date().toISOString() };
        const { error } = await sb.rpc('append_lead_note', { p_id: lead.id, p_note: note });
        if (error) throw error;

      } else if (type === 'tarefa' || type === 'reuniao') {
        const { error } = await sb.from('events').insert({
          title: title, cat: type === 'tarefa' ? 'task' : 'meet',
          client: conv?.name || null, phone, sub: text || null,
          start_at: toISO(date, time), end_at: toISO(date, time, 1),
          user_id: uid, workspace_id: wsId,
        });
        if (error) throw error;

      } else if (type === 'lembrete') {
        const remindAt = toISO(date, time);
        const fmtDate  = date
          ? new Date(`${date}T${time || '09:00'}:00`).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' })
          : null;
        const { error } = await sb.from('notifications').insert({
          user_id: uid, module: 'sistema', type: 'lembrete', priority: 'normal',
          title: 'Lembrete' + (conv?.name ? ` · ${conv.name}` : ''),
          body: text + (fmtDate ? ` — ${fmtDate}` : ''),
          is_read: false, expires_at: remindAt,
        });
        if (error) throw error;
        window.HorosNotif?.notify?.({ title: 'Lembrete criado', body: text });
      }

      setSaved(true);
      setTimeout(onClose, 1100);
    } catch(e) {
      setErr('Ocorreu um erro ao salvar. Tente novamente ou fale com o suporte.');
    }
    setSaving(false);
  }

  const S = {
    input: { width: '100%', padding: '9px 12px', fontSize: 13.5, border: '1px solid var(--border)',
      borderRadius: 8, fontFamily: 'var(--font)', background: 'var(--bg-1)', color: 'var(--fg)',
      outline: 'none', boxSizing: 'border-box' },
    label: { display: 'block', fontSize: 11, fontWeight: 700, color: 'var(--fg-4)',
      marginBottom: 5, letterSpacing: '0.06em', textTransform: 'uppercase' },
  };

  const isAgenda = type === 'tarefa' || type === 'reuniao';

  return (
    <div style={{ position: 'fixed', inset: 0, zIndex: 9999, background: 'rgba(0,0,0,0.48)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', backdropFilter: 'blur(3px)' }}
      onClick={e => { if (e.target === e.currentTarget) onClose(); }}>
      <div style={{ background: 'var(--bg-2)', borderRadius: 16, width: '100%', maxWidth: 420,
        boxShadow: '0 24px 64px rgba(0,0,0,0.4)', overflow: 'hidden', fontFamily: 'var(--font)' }}>

        {/* Header */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '18px 20px 14px',
          borderBottom: '1px solid var(--border)' }}>
          <div style={{ width: 36, height: 36, borderRadius: 10, flexShrink: 0,
            background: cfg.accent + '1a', display: 'grid', placeItems: 'center', color: cfg.accent }}>
            <Icon name={cfg.icon} size={15} />
          </div>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 15, fontWeight: 700, color: 'var(--fg)' }}>{cfg.label}</div>
            {conv && <div style={{ fontSize: 12, color: 'var(--fg-4)', marginTop: 1 }}>
              {conv.name}{lead ? ' · Lead' : ''}
            </div>}
          </div>
          <button onClick={onClose} style={{ width: 28, height: 28, border: 'none', background: 'var(--bg-2)',
            borderRadius: 7, cursor: 'pointer', display: 'grid', placeItems: 'center', color: 'var(--fg-3)' }}>
            <MIcon name="xmark" size={13} />
          </button>
        </div>

        {/* Body */}
        <div style={{ padding: '18px 20px', display: 'flex', flexDirection: 'column', gap: 14 }}>
          {saved ? (
            <div style={{ textAlign: 'center', padding: '20px 0' }}>
              <svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="#10b981" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                <circle cx="12" cy="12" r="10"/><polyline points="20 6 9 17 4 12"/>
              </svg>
              <div style={{ marginTop: 8, fontSize: 14, fontWeight: 600, color: 'var(--fg)' }}>Salvo!</div>
              {isAgenda && (
                <a href="calendar.html" style={{ display: 'inline-block', marginTop: 10, fontSize: 12.5,
                  color: cfg.accent, textDecoration: 'none', fontWeight: 500 }}>
                  Ver na Agenda →
                </a>
              )}
            </div>
          ) : (
            <>
              {cfg.needsTitle && (
                <div>
                  <label style={S.label}>Título *</label>
                  <input style={S.input} value={title} onChange={e => setTitle(e.target.value)}
                    placeholder={type === 'tarefa' ? 'Ex: Enviar proposta' : type === 'proposta' ? `Ex: Proposta ${segTerm('product', 'imóvel')} X` : 'Ex: Reunião de apresentação'}
                    autoFocus />
                </div>
              )}
              {cfg.needsText && (
                <div>
                  <label style={S.label}>{type === 'nota' ? 'Nota *' : type === 'lembrete' ? 'Mensagem *' : 'Observações'}</label>
                  <textarea style={{ ...S.input, resize: 'vertical', minHeight: 88, lineHeight: 1.5 }}
                    value={text} onChange={e => setText(e.target.value)}
                    placeholder={type === 'nota' ? 'Escreva uma nota sobre este contato…' : type === 'lembrete' ? 'O que você quer ser lembrado?' : 'Detalhes…'}
                    autoFocus={!cfg.needsTitle} />
                </div>
              )}
              {cfg.needsDate && (
                <div style={{ display: 'flex', gap: 10 }}>
                  <div style={{ flex: 1 }}>
                    <label style={S.label}>Data</label>
                    <input type="date" style={S.input} value={date} onChange={e => setDate(e.target.value)} />
                  </div>
                  {cfg.needsTime && (
                    <div style={{ width: 115 }}>
                      <label style={S.label}>Hora</label>
                      <input type="time" style={S.input} value={time} onChange={e => setTime(e.target.value)} />
                    </div>
                  )}
                </div>
              )}
              {cfg.needsPrio && (
                <div>
                  <label style={S.label}>Prioridade</label>
                  <div style={{ display: 'flex', gap: 8 }}>
                    {[['alta','#ef4444'],['normal','#f59e0b'],['baixa','#6b7280']].map(([p,c]) => (
                      <button key={p} onClick={() => setPriority(p)} style={{
                        flex: 1, padding: '7px 0', borderRadius: 7,
                        border: `1.5px solid ${priority===p ? c : 'var(--border)'}`,
                        background: priority===p ? c+'1a' : 'transparent',
                        color: priority===p ? c : 'var(--fg-4)',
                        fontSize: 12.5, fontWeight: 600, cursor: 'pointer',
                        textTransform: 'capitalize', fontFamily: 'var(--font)',
                      }}>{p.charAt(0).toUpperCase()+p.slice(1)}</button>
                    ))}
                  </div>
                </div>
              )}
              {err && <div style={{ fontSize: 12.5, color: '#ef4444', background: 'rgba(239,68,68,0.08)',
                border: '1px solid rgba(239,68,68,0.2)', borderRadius: 8, padding: '8px 12px' }}>{err}</div>}
            </>
          )}
        </div>

        {/* Footer */}
        {!saved && (
          <div style={{ padding: '0 20px 18px', display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
            <button onClick={onClose} style={{ padding: '8px 18px', borderRadius: 8,
              border: '1px solid var(--border)', background: 'transparent', color: 'var(--fg-3)',
              fontSize: 13.5, fontWeight: 500, cursor: 'pointer', fontFamily: 'var(--font)' }}>
              Cancelar
            </button>
            <button onClick={handleSave} disabled={!isValid || saving} style={{
              padding: '8px 20px', borderRadius: 8, border: 'none',
              background: isValid && !saving ? cfg.accent : 'var(--bg-3)',
              color: isValid && !saving ? '#fff' : 'var(--fg-4)',
              fontSize: 13.5, fontWeight: 600, cursor: isValid && !saving ? 'pointer' : 'default',
              fontFamily: 'var(--font)', transition: 'background 150ms', minWidth: 80,
            }}>{saving ? 'Salvando…' : 'Salvar'}</button>
          </div>
        )}
      </div>
    </div>
  );
}

/* ── PanelPill — painel colapsado ───────────────────────────────────────── */

function _PanelPill({ conv, lead, onToggle, onOpenDrawer, onAction }) {
  const isGroup = (conv.jid || '').endsWith('@g.us');
  const ACTIONS = [
    { icon: 'pencil',  label: 'Nota',    key: 'nota'     },
    { icon: 'check',   label: 'Tarefa',  key: 'tarefa'   },
    { icon: 'cal',     label: 'Reunião', key: 'reuniao'  },
    { icon: 'bell',    label: 'Lembrete',key: 'lembrete' },
  ];
  return (
    <div className="mc-panel-pill">
      {/* Avatar do contato — oculto em grupos */}
      {!isGroup && (
        <div className="mc-panel-pill-avatar">
          <MsgAvatar name={conv.name} pic={conv.pic} size={32} phone={conv.phone} />
        </div>
      )}

      <div className="mc-panel-pill-sep" />

      {/* Abrir ficha (se lead) */}
      {lead && (
        <button className="mc-panel-pill-btn" title={`Abrir ficha — ${conv.name}`}
          onClick={() => onOpenDrawer && onOpenDrawer(lead.id)}>
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"
            strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
            <circle cx="12" cy="7" r="4"/>
          </svg>
        </button>
      )}

      {/* Ações rápidas */}
      {ACTIONS.map(a => (
        <button key={a.key} className="mc-panel-pill-btn" title={a.label}
          onClick={() => onAction && onAction(a.key)}>
          <Icon name={a.icon} size={13} />
        </button>
      ))}

    </div>
  );
}

/* ── Contato com número oculto (@lid): ligar a um número/lead ───────────────
   O WhatsApp esconde o número de contatos com privacidade; sem o número, as etiquetas
   não chegam ao lead no Kanban. O servidor liga sozinho quando consegue (foto/recado ou
   quando o contato manda mensagem); aqui o corretor resolve na hora digitando o número
   ou escolhendo o lead. POST /api/wa-lid-link { action:'link', lid, phone } */
function _LidLinkBox({ conv, onLinked }) {
  const [q, setQ]         = React.useState('');
  const [leads, setLeads] = React.useState([]);
  const [busy, setBusy]   = React.useState(false);
  const [err, setErr]     = React.useState('');
  const digits = q.replace(/\D/g, '');
  const isPhone = digits.length >= 10 && digits.length <= 13 && !/[a-zA-Z]/.test(q);

  React.useEffect(() => { setQ(''); setLeads([]); setErr(''); }, [conv?.id]);
  React.useEffect(() => {
    const term = q.trim();
    if (isPhone || term.length < 2) { setLeads([]); return; }
    let off = false;
    const t = setTimeout(async () => {
      const { data } = await window.HOROS_SB.from('leads').select('id, name, phone')
        .eq('archived', false).ilike('name', `%${term.replace(/[%,()]/g, ' ')}%`).limit(6);
      if (!off) setLeads((data || []).filter(l => String(l.phone || '').replace(/\D/g, '').length >= 10));
    }, 250);
    return () => { off = true; clearTimeout(t); };
  }, [q, isPhone]);

  async function link(phone) {
    if (busy) return;
    setBusy(true); setErr('');
    try {
      const tok = window.horosToken ? await window.horosToken() : window.HOROS_SESSION?.access_token;
      const r = await fetch('/api/wa-lid-link', {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${tok}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'link', lid: conv.jid, phone }),
      });
      const d = await r.json().catch(() => ({}));
      if (!r.ok || !d.ok) throw new Error(d.error || 'Não foi possível vincular.');
      window.kbWaLoad && window.kbWaLoad(true);
      onLinked && onLinked(d.convId || conv.id);
    } catch (e) { setErr(e.message); }
    setBusy(false);
  }

  return (
    <div className="mc-rp2-row mc-lidlink">
      <div className="mc-rp2-lbl">NÚMERO OCULTO PELO WHATSAPP</div>
      <div className="mc-lidlink-hint">
        Informe o número (ou escolha o lead) para as etiquetas aparecerem no CRM.
      </div>
      <div className="mc-lidlink-form">
        <input value={q} onChange={e => { setQ(e.target.value); setErr(''); }} disabled={busy}
          onKeyDown={e => { if (e.key === 'Enter' && isPhone) link(digits); }}
          placeholder="(11) 99999-9999 ou nome do lead" />
        <button onClick={() => link(digits)} disabled={!isPhone || busy}>
          {busy ? 'Vinculando…' : 'Vincular'}
        </button>
      </div>
      {leads.length > 0 && (
        <div className="mc-lidlink-list">
          {leads.map(l => (
            <button key={l.id} onClick={() => link(l.phone)} disabled={busy}>
              <span>{l.name}</span>
              <span className="mc-lidlink-ph">{window.fmtPhone ? window.fmtPhone(l.phone) : l.phone}</span>
            </button>
          ))}
        </div>
      )}
      {err && <div className="mc-lidlink-err">{err}</div>}
    </div>
  );
}

/* ── RightPanel ──────────────────────────────────────────────────────────── */

// Rodízio: corretor que não é mais o responsável vê/move a PRÓPRIA etapa (lead_assignments), não leads.col
async function _withOwnStage(lead) {
  const ws = window.HOROS_WORKSPACE;
  const role = ws?.lead_role || ws?.role;
  const me = window.horosActUid ? window.horosActUid() : window.HOROS_USER?.id;
  if (!lead || !window.enrichLeadStages || !['corretor', 'assistente'].includes(role) || lead.assigned_to === me) return lead;
  try { return (await window.enrichLeadStages([lead]))[0] || lead; } catch { return lead; }
}

function _RightPanel({ conv, lead, waLabels, waPhone, onLeadCreated, onOpenDrawer, onMoveCol, onToggleLabel, suggestion, onSuggestionChange, onAction, onRenameConv, copilotOn, onLidLinked }) {
  const originLead = (window.useLeadOrigin || ((l) => l))(lead);
  const [addingLead, setAddingLead]   = React.useState(false);
  const [addErr, setAddErr]           = React.useState('');
  const [showAddLabel, setShowAddLabel] = React.useState(false);
  const [editingName, setEditingName] = React.useState(false);
  const [nameInput, setNameInput]     = React.useState('');
  const [savingName, setSavingName]   = React.useState(false);
  const nameInputRef = React.useRef(null);
  const addLabelRef = React.useRef(null);

  // Vários funis: etapas do funil do lead; "funil|etapa" no seletor move de funil também
  const pipes = window.usePipelines ? window.usePipelines() : { pipelines: [] };
  const leadPid = lead && window.leadPipeline ? window.leadPipeline(lead) : 'main';
  const kanbanCols = window.useKanbanCols ? window.useKanbanCols(leadPid) : [];

  React.useEffect(() => {
    if (!showAddLabel) return;
    function handler(e) {
      if (addLabelRef.current && !addLabelRef.current.contains(e.target)) setShowAddLabel(false);
    }
    document.addEventListener('mousedown', handler);
    return () => document.removeEventListener('mousedown', handler);
  }, [showAddLabel]);

  React.useEffect(() => {
    if (editingName) setTimeout(() => nameInputRef.current?.focus(), 50);
  }, [editingName]);

  async function saveName() {
    const trimmed = nameInput.trim();
    if (!trimmed || !conv?.id) return;
    setSavingName(true);
    const { error } = await window.HOROS_SB
      .from('conversations').update({ contact_name: trimmed }).eq('id', conv.id);
    setSavingName(false);
    if (!error) { setEditingName(false); onRenameConv?.(conv.id, trimmed); }
  }

  if (!conv) return (
    <div className="mc-right" style={{ alignItems: 'center', justifyContent: 'center' }}>
      <div style={{ textAlign: 'center', color: 'var(--fg-4)', fontSize: 13 }}>
        Selecione uma conversa
      </div>
    </div>
  );

  // BUG 2 FIX: show full phone number using the global fmtPhone helper
  function maskPhone(raw) {
    const digits = (raw || '').replace(/\D/g, '');
    if (!digits) return raw || '—';
    if (typeof window.fmtPhone === 'function') return window.fmtPhone(digits);
    return _maskPhone(digits);
  }

  function fmtCreatedAt(ts) {
    if (!ts) return '—';
    const d = new Date(ts);
    return d.toLocaleDateString('pt-BR') + ', ' + d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
  }

  async function addAsLead() {
    if (addingLead) return;
    setAddingLead(true); setAddErr('');
    try {
      const profile = window.HOROS_USER?.user_metadata;
      const seg = profile?.area || 'mcmv';
      const nm  = conv.unnamed ? '' : (conv.name || '');
      const ini = (typeof initials === 'function' && initials(nm)) || 'CH';
      // Só o telefone real — contato @lid (privacidade) não tem número; o LID não é telefone
      const ph  = (conv.phone || '').replace(/\D/g, '');
      if (ph.length < 10) {
        setAddErr('O WhatsApp não mostra o número deste contato (privacidade). Ele aparece assim que o contato mandar uma mensagem.');
        setAddingLead(false);
        return;
      }
      const { data, error } = await window.HOROS_SB
        .rpc('insert_wa_lead', { p_name: nm, p_initials: ini, p_phone: ph, p_seg: seg });
      if (!error) window.dispatchEvent(new CustomEvent('horos:tour-check', { detail: { step: 'lead' } }));
      if (error) {
        console.warn('[addAsLead]', error);
        setAddErr('Não foi possível criar o lead: ' + (error.message || 'erro desconhecido'));
        setAddingLead(false);
        return;
      }
      if (data?.error === 'duplicate') {
        setAddErr(`Já existe um lead com este número (pode estar com outro ${segTerm('agent', 'corretor')}).`);
        setAddingLead(false);
        // Carrega e exibe o lead existente no painel
        const digits2 = ph.replace(/^55/, '');
        const mask2   = _maskPhone(digits2);
        const full55_2 = '55' + digits2;
        const { data: existing } = await window.HOROS_SB.from('leads').select('*')
          .eq('archived', false).in('phone', [ph, digits2, mask2, full55_2]).limit(1);
        if (existing?.[0]) onLeadCreated && onLeadCreated(existing[0]);
        return;
      }
      // Só vira lead no painel — a ficha não abre sozinha (abre pelo "Abrir ficha completa")
      onLeadCreated && onLeadCreated(data);
    } catch (e) { setAddErr('Ocorreu um erro. Tente novamente ou fale com o suporte.'); }
    setAddingLead(false);
  }

  async function moveCol(colId) {
    if (!lead) return;
    if (colId.includes('|')) {
      const [pid, col] = colId.split('|');
      try {
        await window.horosMoveLeadsPipeline([lead.id], pid, col); onMoveCol && onMoveCol(lead.id, col, pid === 'main' ? null : pid);
        if (horosIsWonCol(col, pid)) horosAskSale(lead.id);
      }
      catch (e) { window.dispatchEvent(new CustomEvent('horos:toast', { detail: { module: 'system', title: 'Não foi possível mudar o funil', body: e.message } })); }
      return;
    }
    const prevCol = lead.col;
    onMoveCol && onMoveCol(lead.id, colId);
    const { data: ok, error } = await window.HOROS_SB.rpc('move_lead', { p_id: lead.id, p_col: colId });
    if (!error && ok !== false) {
      try { localStorage.setItem('horos-leads-changed-at', String(Date.now())); } catch {}
      const pid = leadPipeline(lead);
      if (horosIsWonCol(colId, pid)) horosAskSale(lead.id);
      if (window.horosIsApprovedCol?.(colId, pid)) window.horosAskApproval?.(lead.id);
    } else {
      onMoveCol && onMoveCol(lead.id, prevCol);
      window.dispatchEvent(new CustomEvent('horos:toast', { detail: { module: 'system',
        title: 'Não foi possível mudar a etapa', body: error?.message || 'Tente de novo.' } }));
    }
  }

  const activeLabels   = _waTagLabels(waLabels).filter(l => (conv.labels || []).includes(l.label_id));
  const inactiveLabels = _waTagLabels(waLabels).filter(l => !(conv.labels || []).includes(l.label_id));
  const maskedPhone    = maskPhone(conv.phone || conv.jid?.split('@')[0] || '');
  const conexao        = waPhone ? `WhatsApp ${maskPhone(waPhone)}` : 'WhatsApp';

  return (
    <div className="mc-right">
      {/* ── Header: avatar centrado + nome + telefone mascarado ── */}
      <div className="mc-rp2-head">
        {lead && (
          <button className="mc-rp2-edit-btn" onClick={() => onOpenDrawer?.(lead.id)} title="Editar ficha">
            <MIcon name="pencil" size={14} />
          </button>
        )}
        <div className="mc-rp2-avatar-wrap">
          <MsgAvatar name={conv.name} pic={conv.pic} size={64} phone={conv.phone} />
        </div>
        {editingName ? (
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '0 8px', width: '100%', boxSizing: 'border-box' }}>
            <input
              ref={nameInputRef}
              value={nameInput}
              onChange={e => setNameInput(e.target.value)}
              onKeyDown={e => { if (e.key === 'Enter') saveName(); if (e.key === 'Escape') setEditingName(false); }}
              placeholder="Nome do contato"
              style={{ flex: 1, background: 'var(--bg-input, #23262d)', border: '1px solid var(--accent)', borderRadius: 8, padding: '5px 10px', color: 'var(--fg)', fontSize: 14, outline: 'none' }}
            />
            <button onClick={saveName} disabled={savingName || !nameInput.trim()}
              style={{ background: 'var(--accent)', color: '#fff', border: 'none', borderRadius: 8, padding: '5px 12px', cursor: 'pointer', fontSize: 13, opacity: (!nameInput.trim() || savingName) ? 0.5 : 1 }}>
              {savingName ? '...' : 'Salvar'}
            </button>
            <button onClick={() => setEditingName(false)}
              style={{ background: 'transparent', border: 'none', color: 'var(--fg-3)', cursor: 'pointer', fontSize: 18, lineHeight: 1 }}>×</button>
          </div>
        ) : (
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, justifyContent: 'center' }}>
            <div className="mc-rp2-name">{conv.name}</div>
            <button onClick={() => { setNameInput((conv.name === 'Desconhecido' || conv.unnamed) ? '' : conv.name); setEditingName(true); }}
              title="Editar nome" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--fg-4)', padding: 2, lineHeight: 1, display: 'flex', alignItems: 'center' }}>
              <MIcon name="pencil" size={13} />
            </button>
          </div>
        )}
        <div className="mc-rp2-phone" style={{display:'flex',alignItems:'center',gap:8,flexWrap:'wrap'}}>
          <span>{maskedPhone}</span>
          {conv.phone && window.LeadCallButton && (
            <window.LeadCallButton lead={{ id: lead?.id, phone: conv.phone }} />
          )}
        </div>
        {lead && window.LeadOriginBadge && <div className="mc-rp2-origin"><window.LeadOriginBadge lead={originLead || lead} />{window.LeadHandoffNote && <window.LeadHandoffNote lead={originLead || lead} />}</div>}
      </div>

      {/* ── Body: linhas label/valor ── */}
      <div className="mc-rp2-body">

        {/* Contato @lid sem número: ligar a um número/lead (etiquetas chegam ao Kanban) */}
        {!lead && !conv.phone && String(conv.jid || '').endsWith('@lid') && (
          <_LidLinkBox conv={conv} onLinked={onLidLinked} />
        )}

        {/* Botão "Adicionar como Lead" — só quando não é lead */}
        {!lead && (
          <div className="mc-rp2-row">
            <button className="mc-rp2-add-lead-btn" onClick={addAsLead} disabled={addingLead}>
              {addingLead ? 'Adicionando…' : 'Adicionar como Lead'}
            </button>
            {addErr && <div style={{ fontSize: 11, color: '#ef4444', marginTop: 5 }}>{addErr}</div>}
          </div>
        )}

        {/* ETIQUETAS — sempre visível */}
        {waLabels.length > 0 && (
          <div className="mc-rp2-row">
            <div className="mc-rp2-lbl">ETIQUETAS</div>
            <div className="mc-rp2-tags" ref={addLabelRef}>
              {activeLabels.map(lbl => {
                const color = _waLabelColor(lbl.color);
                return (
                  <span key={lbl.label_id} className="mc-rp2-pill"
                    style={{ color, borderColor: `${color}50`, background: `${color}14` }}>
                    {lbl.name}
                    {onToggleLabel && (
                      <button className="mc-rp2-pill-x"
                        onClick={() => onToggleLabel(lbl.label_id, true)}>
                        <MIcon name="xmark" size={9} />
                      </button>
                    )}
                  </span>
                );
              })}
              {activeLabels.length === 0 && (
                <span style={{ fontSize: 12, color: 'var(--fg-4)' }}>Nenhuma</span>
              )}
              {inactiveLabels.length > 0 && onToggleLabel && (
                <span className="mc-rp2-add-pill" onClick={() => setShowAddLabel(v => !v)}>
                  + Adicionar
                </span>
              )}
              {showAddLabel && (
                <div className="mc-rp2-label-picker">
                  {inactiveLabels.map(lbl => {
                    const color = _waLabelColor(lbl.color);
                    return (
                      <button key={lbl.label_id} className="mc-rp2-label-pick-item"
                        onClick={() => { onToggleLabel?.(lbl.label_id, false); setShowAddLabel(false); }}>
                        <span style={{ width: 8, height: 8, borderRadius: '50%', background: color, flexShrink: 0 }} />
                        <span style={{ color: 'var(--fg)' }}>{lbl.name}</span>
                      </button>
                    );
                  })}
                </div>
              )}
            </div>
          </div>
        )}

        {/* ── Campos exclusivos de leads ── */}
        {lead && (
          <>
            {/* ATENDENTE */}
            {lead.broker && (
              <div className="mc-rp2-row">
                <div className="mc-rp2-lbl">ATENDENTE</div>
                <div className="mc-rp2-val">{lead.broker}</div>
              </div>
            )}

            {/* ETAPA */}
            {kanbanCols.length > 0 && (
              <div className="mc-rp2-row">
                <div className="mc-rp2-lbl">ETAPA</div>
                <select className="mc-rp2-select" value={lead.col || ''} onChange={e => moveCol(e.target.value)}>
                  {pipes.pipelines.length > 0 && !lead._prevHolder && (!lead.assigned_to || lead.assigned_to === (window.horosActUid ? window.horosActUid() : lead.assigned_to))
                    ? [{ id: 'main', name: pipes.mainName }, ...pipes.pipelines].map(p => (
                        <optgroup key={p.id} label={p.name}>
                          {window.pipelineCols(p.id).map(c => (
                            <option key={c.id} value={p.id === leadPid ? c.id : `${p.id}|${c.id}`}>{c.title || c.name}</option>
                          ))}
                        </optgroup>
                      ))
                    : kanbanCols.map(c => <option key={c.id} value={c.id}>{c.title || c.name}</option>)}
                </select>
              </div>
            )}

            {/* CRIADO EM */}
            <div className="mc-rp2-row">
              <div className="mc-rp2-lbl">CRIADO EM</div>
              <div className="mc-rp2-val">{fmtCreatedAt(conv.created_at || lead.created_at)}</div>
            </div>

            {/* CONEXÃO */}
            <div className="mc-rp2-row">
              <div className="mc-rp2-lbl">CONEXÃO</div>
              <div className="mc-rp2-val">{conexao}</div>
            </div>
          </>
        )}

        {/* Ações rápidas */}
        <div className="mc-rp2-row">
          <div className="mc-rp2-lbl">AÇÕES RÁPIDAS</div>
          <div className="mc-quick" style={{ marginTop: 6 }}>
            {[{ icon: 'message', label: 'Proposta', key: 'proposta' },
              { icon: 'cal',     label: 'Agenda',   key: 'reuniao'  },
              { icon: 'pencil',  label: 'Nota',     key: 'nota'     },
              { icon: 'bell',    label: 'Lembrete', key: 'lembrete' }].map(a => (
              <button key={a.key} className="mc-quick-btn"
                onClick={() => onAction && onAction(a.key)}>
                <Icon name={a.icon} size={13} className="ico" />
                {a.label}
              </button>
            ))}
          </div>
        </div>

        {/* Intelligence panel */}
        {copilotOn && window.HorosIntelligencePanel && React.createElement(window.HorosIntelligencePanel, {
          conv, suggestion, intellStatus: conv?.intellStatus, onSuggestionChange,
        })}

      </div>
    </div>
  );
}

/* ── ClientDrawer ────────────────────────────────────────────────────────── */

function _ClientDrawer({ leadId, onClose }) {
  // "Salvar e fechar" / "Voltar" dentro da ficha fecham o painel
  React.useEffect(() => {
    const onMsg = (e) => { if (e.origin === location.origin && e.data?.type === 'horos:embed-close') onClose(); };
    window.addEventListener('message', onMsg);
    return () => window.removeEventListener('message', onMsg);
  }, [onClose]);
  return (
    <>
      <div className="mc-client-drawer-overlay" onClick={onClose} />
      <div className="mc-client-drawer">
        <button className="mc-client-drawer-close" onClick={onClose}>
          <MIcon name="xmark" size={16} />
        </button>
        <iframe
          className="mc-client-drawer-frame"
          src={`client.html?id=${leadId}&embed=1`}
          title="Editar cliente"
          allow="fullscreen"
        />
      </div>
    </>
  );
}

/* ── QuickRepliesPanel ───────────────────────────────────────────────────── */

// Fora do imobiliário, troca as respostas que falam de imóvel, visita e financiamento (2026-10-13)
function _qrDefaults() {
  const g = window.horosSeg ? window.horosSeg() : null;
  if (!g || g.id === 'imobiliario') return _QR_DEFAULTS;
  const meet = g.meet1 || 'reunião';
  const swap = {
    dr3: { shortcut: 'agendar', message: 'Que ótimo interesse! Me confirme o melhor dia e horário para marcarmos ' + (/(a|ão|gem)$|^aula/.test(meet) ? 'a ' : 'o ') + meet + '.' },
    dr4: { shortcut: 'documentos', message: 'Para dar andamento, vou precisar de alguns dados: nome completo, CPF e um e-mail. Consegue me enviar?' },
    dr5: { shortcut: 'proposta', message: 'Vou preparar uma proposta para você. Me conta o que é mais importante na sua escolha?' },
    dr6: { shortcut: 'retorno', message: 'Recebi tudo certinho! Te dou um retorno assim que tiver a resposta, normalmente em até 24h! 👍' },
  };
  return _QR_DEFAULTS.map(q => swap[q.id] ? { id: q.id, ...swap[q.id] } : q);
}
const _QR_DEFAULTS = [
  { id: 'dr1', shortcut: 'saudacao',        message: 'Olá! Tudo bem? Como posso te ajudar hoje? 😊' },
  { id: 'dr2', shortcut: 'disponibilidade', message: 'Tenho disponibilidade de seg a sáb, das 9h às 18h. Qual horário fica melhor para você?' },
  { id: 'dr3', shortcut: 'visita',          message: 'Que ótimo interesse! Me confirme o melhor dia e horário para agendar a visita ao imóvel.' },
  { id: 'dr4', shortcut: 'documentos',      message: 'Para dar andamento, vou precisar de: RG, CPF, comprovante de renda e de residência. Consegue reunir?' },
  { id: 'dr5', shortcut: 'financiamento',   message: 'Trabalhamos com FGTS e MCMV. Posso verificar as melhores opções para o seu perfil. Qual é a sua renda mensal aproximada?' },
  { id: 'dr6', shortcut: 'proposta',        message: 'Recebi sua proposta e já estou encaminhando ao proprietário. Te informo assim que tiver retorno, normalmente em até 24h! 👍' },
  { id: 'dr7', shortcut: 'aguarde',         message: 'Estou verificando as informações para você agora. Por favor, aguarde um momento. ⏳' },
  { id: 'dr8', shortcut: 'obrigado',        message: 'Foi um prazer te atender! Qualquer dúvida é só chamar. Tenha um ótimo dia! 😊' },
];

let _qrCache = null;
const _QR_BUCKET = 'quick-replies';

function _qrMediaType(mime) {
  const m = String(mime || '');
  return m.startsWith('image/') ? 'image' : m.startsWith('video/') ? 'video' : m.startsWith('audio/') ? 'audio' : 'document';
}
function _qrMedia(r) { return Array.isArray(r?.media) ? r.media : []; }

// Carrega as respostas rápidas do usuário (cria as padrão na primeira vez)
async function _qrLoad(force) {
  if (_qrCache && !force) return _qrCache;
  const sb = window.HOROS_SB;
  const userId = window.HOROS_SESSION?.user?.id || window.HOROS_USER?.id;
  const { data, error } = await sb.from('quick_replies').select('*').order('created_at');
  if (error) return _qrCache || [];
  if (data && data.length > 0) { _qrCache = data; return data; }
  const seeds = _qrDefaults().map(({ shortcut, message }) => ({ shortcut, message, user_id: userId }));
  const { data: inserted } = await sb.from('quick_replies').insert(seeds).select();
  _qrCache = inserted || [];
  return _qrCache;
}

// Baixa os anexos de uma resposta rápida (bucket privado) como File — prontos para o envio normal
const _qrBlobCache = new Map();
async function _qrMediaFiles(reply) {
  const out = [];
  for (const m of _qrMedia(reply)) {
    let blob = _qrBlobCache.get(m.path);
    if (!blob) {
      const { data, error } = await window.HOROS_SB.storage.from(_QR_BUCKET).download(m.path);
      if (error || !data) throw new Error(m.fileName || 'arquivo');
      blob = data;
      _qrBlobCache.set(m.path, blob);
    }
    const f = new File([blob], m.fileName || 'arquivo', { type: m.mimetype || blob.type || 'application/octet-stream' });
    f.__qrRef = { bucket: _QR_BUCKET, path: m.path };   // envio usa o arquivo já guardado (sem limite de tamanho)
    out.push(f);
  }
  return out;
}

function _qrMediaSummary(media) {
  const n = { image: 0, video: 0, audio: 0, document: 0 };
  media.forEach(m => { n[m.mediaType || _qrMediaType(m.mimetype)]++; });
  return [
    n.image    && { icon: 'img',   label: n.image    === 1 ? '1 foto'    : `${n.image} fotos` },
    n.video    && { icon: 'video', label: n.video    === 1 ? '1 vídeo'   : `${n.video} vídeos` },
    n.audio    && { icon: 'mic',   label: n.audio    === 1 ? '1 áudio'   : `${n.audio} áudios` },
    n.document && { icon: 'doc',   label: n.document === 1 ? '1 arquivo' : `${n.document} arquivos` },
  ].filter(Boolean);
}

function _QrMediaBadges({ media }) {
  const parts = _qrMediaSummary(media || []);
  if (!parts.length) return null;
  return (
    <span className="mc-qr-media-badges">
      {parts.map(p => (
        <span key={p.icon} className="mc-qr-media-badge"><MIcon name={p.icon} size={11} />{p.label}</span>
      ))}
    </span>
  );
}

function _QuickRepliesPanel({ onClose, onSelectReply }) {
  const sb     = window.HOROS_SB;
  const userId = window.HOROS_SESSION?.user?.id || window.HOROS_USER?.id;
  const [replies, setReplies]       = React.useState([]);
  const [loading, setLoading]       = React.useState(true);
  const [mode, setMode]             = React.useState('new');
  const [editingId, setEditingId]   = React.useState(null);
  const [editOrig, setEditOrig]     = React.useState('');
  const [shortcut, setShortcut]     = React.useState('');
  const [message, setMessage]       = React.useState('');
  const [err, setErr]               = React.useState('');
  const [deletingId, setDeletingId] = React.useState(null);
  const inputRef = React.useRef();

  // Anexos: media = já salvos · newFiles = escolhidos agora (sobem ao salvar) · removed = paths a apagar
  const [media, setMedia]       = React.useState([]);
  const [newFiles, setNewFiles] = React.useState([]);
  const [removed, setRemoved]   = React.useState([]);
  const [saving, setSaving]     = React.useState(false);
  const fileInRef = React.useRef();

  React.useEffect(() => {
    _qrLoad().then(list => { setReplies(list); setLoading(false); });
  }, []);

  function resetMedia() { setMedia([]); setNewFiles([]); setRemoved([]); }

  function onPickFiles(e) {
    const files = Array.from(e.target.files || []);
    e.target.value = '';
    const big = files.find(f => f.size > 50 * 1024 * 1024);
    if (big) { setErr(`"${big.name}" passa de 50 MB`); return; }
    setNewFiles(prev => [...prev, ...files]);
    setErr('');
  }

  async function uploadNew() {
    const done = [];
    try {
      for (const f of newFiles) {
        const safe = (f.name || 'arquivo').normalize('NFD').replace(/[^\w.\-]/g, '_');
        const path = `${userId}/${crypto.randomUUID()}/${safe}`;
        const { error } = await sb.storage.from(_QR_BUCKET).upload(path, f, { contentType: f.type || 'application/octet-stream' });
        if (error) throw error;
        done.push({ path, mediaType: _qrMediaType(f.type), mimetype: f.type || 'application/octet-stream', fileName: f.name, size: f.size });
      }
      return done;
    } catch (e) {
      if (done.length) sb.storage.from(_QR_BUCKET).remove(done.map(d => d.path));
      throw e;
    }
  }

  function startEdit(r) {
    setMode('edit'); setEditingId(r.id); setEditOrig(r.shortcut);
    setShortcut(r.shortcut); setMessage(r.message || ''); setErr('');
    setMedia(_qrMedia(r)); setNewFiles([]); setRemoved([]);
    setDeletingId(null);
    setTimeout(() => inputRef.current?.focus(), 30);
  }

  function cancelEdit() {
    setMode('new'); setEditingId(null); setEditOrig('');
    setShortcut(''); setMessage(''); setErr('');
    resetMedia();
  }

  async function saveEdit() {
    const s = shortcut.trim().replace(/^\/+/, '').replace(/\s+/g, '_');
    const m = message.trim();
    if (!s) { setErr('Preencha o atalho'); return; }
    if (!m && media.length + newFiles.length === 0) { setErr('Escreva a mensagem ou anexe um arquivo'); return; }
    if (replies.find(r => r.shortcut.toLowerCase() === s.toLowerCase() && r.id !== editingId))
      { setErr(`Atalho "/${s}" já existe`); return; }
    if (saving) return;
    setSaving(true);
    let uploaded = [];
    try { uploaded = await uploadNew(); }
    catch { setSaving(false); setErr('Erro ao enviar o anexo'); return; }
    const finalMedia = [...media, ...uploaded];
    const { error } = await sb.from('quick_replies').update({ shortcut: s, message: m, media: finalMedia }).eq('id', editingId);
    setSaving(false);
    if (error) {
      if (uploaded.length) sb.storage.from(_QR_BUCKET).remove(uploaded.map(u => u.path));
      setErr('Erro ao salvar'); return;
    }
    if (removed.length) sb.storage.from(_QR_BUCKET).remove(removed);
    const newList = replies.map(r => r.id === editingId ? { ...r, shortcut: s, message: m, media: finalMedia } : r);
    setReplies(newList); _qrCache = newList;
    cancelEdit();
  }

  async function addReply() {
    const s = shortcut.trim().replace(/^\/+/, '').replace(/\s+/g, '_');
    const m = message.trim();
    if (!s) { setErr('Preencha o atalho'); return; }
    if (!m && newFiles.length === 0) { setErr('Escreva a mensagem ou anexe um arquivo'); return; }
    if (replies.find(r => r.shortcut.toLowerCase() === s.toLowerCase()))
      { setErr(`Atalho "/${s}" já existe`); return; }
    if (saving) return;
    setSaving(true);
    let uploaded = [];
    try { uploaded = await uploadNew(); }
    catch { setSaving(false); setErr('Erro ao enviar o anexo'); return; }
    const { data, error } = await sb.from('quick_replies').insert({ shortcut: s, message: m, media: uploaded, user_id: userId }).select().single();
    setSaving(false);
    if (error) {
      if (uploaded.length) sb.storage.from(_QR_BUCKET).remove(uploaded.map(u => u.path));
      setErr('Erro ao salvar'); return;
    }
    const newList = [...replies, data];
    setReplies(newList); _qrCache = newList;
    setShortcut(''); setMessage(''); setErr('');
    resetMedia();
    inputRef.current?.focus();
  }

  async function doDelete(id) {
    const paths = _qrMedia(replies.find(r => r.id === id)).map(m => m.path);
    const { error } = await sb.from('quick_replies').delete().eq('id', id);
    if (error) { setErr('Erro ao apagar'); return; }
    if (paths.length) sb.storage.from(_QR_BUCKET).remove(paths);
    const newList = replies.filter(r => r.id !== id);
    setReplies(newList); _qrCache = newList;
    setDeletingId(null);
    if (editingId === id) cancelEdit();
  }

  function onKey(e) {
    if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) mode === 'edit' ? saveEdit() : addReply();
    if (e.key === 'Escape') { if (mode === 'edit') cancelEdit(); else onClose(); }
  }

  return (
    <div className="mc-qrp-overlay" onClick={onClose}>
      <div className="mc-qrp" onClick={e => e.stopPropagation()} onKeyDown={onKey}>

        {/* Header */}
        <div className="mc-qrp-head">
          <button className="mc-qrp-back" onClick={onClose}>
            <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M19 12H5M12 5l-7 7 7 7"/>
            </svg>
          </button>
          <div>
            <div className="mc-qrp-head-title">Respostas rápidas</div>
            <div className="mc-qrp-head-sub">Digite / no chat para usar · {replies.length} configuradas</div>
          </div>
        </div>

        {/* Body — lista | formulário */}
        <div className="mc-qrp-body">

          {/* Esquerda — lista */}
          <div className={`mc-qrp-list${editingId ? ' locked' : ''}`}>
            {loading ? (
              <SkelList rows={5} avatar={0} pad="12px 16px" />
            ) : replies.map(r => (
              <div key={r.id} className={`mc-qrp-item${editingId === r.id ? ' editing' : editingId ? ' dimmed' : ''}`}>
                {deletingId === r.id ? (
                  <div className="mc-qrp-confirm">
                    <span>Apagar <b>/{r.shortcut}</b>?</span>
                    <div style={{ display: 'flex', gap: 6 }}>
                      <button className="mc-qrp-confirm-yes" onClick={() => doDelete(r.id)}>Apagar</button>
                      <button className="mc-qrp-confirm-no" onClick={() => setDeletingId(null)}>Cancelar</button>
                    </div>
                  </div>
                ) : (
                  <>
                    <div className="mc-qrp-item-left">
                      <span className="mc-qrp-item-shortcut">/{r.shortcut}</span>
                      {r.message && <span className="mc-qrp-item-msg">{r.message}</span>}
                      <_QrMediaBadges media={_qrMedia(r)} />
                    </div>
                    <div className="mc-qrp-item-actions">
                      {onSelectReply && (
                        <button className="mc-qrp-use-btn" title="Usar esta resposta"
                          onClick={() => { onSelectReply(r); onClose(); }}>
                          Usar
                        </button>
                      )}
                      <button className="mc-qrp-act-btn mc-qrp-act-edit" title="Editar" onClick={() => startEdit(r)}>
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                          <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
                          <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
                        </svg>
                      </button>
                      <button className="mc-qrp-act-btn mc-qrp-act-del" title="Apagar" onClick={() => { setDeletingId(r.id); if (editingId === r.id) cancelEdit(); }}>
                        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                          <polyline points="3 6 5 6 21 6"/>
                          <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
                          <path d="M10 11v6M14 11v6"/>
                          <path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
                        </svg>
                      </button>
                    </div>
                  </>
                )}
              </div>
            ))}
          </div>

          {/* Direita — formulário */}
          <div className="mc-qrp-form">
            <div className="mc-qrp-form-title">
              {mode === 'edit'
                ? <span>Editando <span style={{ color: 'var(--accent)' }}>/{editOrig}</span></span>
                : 'Nova resposta rápida'}
            </div>

            <div className="mc-qrp-field">
              <label className="mc-qrp-label">Atalho</label>
              <div className="mc-qrp-shortcut-wrap">
                <span className="mc-qrp-slash">/</span>
                <input ref={inputRef} className="mc-qrp-input" placeholder="saudacao"
                  value={shortcut} onChange={e => { setShortcut(e.target.value); setErr(''); }} />
              </div>
              <span className="mc-qrp-hint">Sem espaços — use _ para separar palavras</span>
            </div>

            <div className="mc-qrp-field">
              <label className="mc-qrp-label">Mensagem</label>
              <textarea className="mc-qrp-textarea" placeholder="Olá! Como posso te ajudar hoje? 😊"
                value={message} onChange={e => { setMessage(e.target.value); setErr(''); }} rows={5} />
              <span className="mc-qrp-hint">
                {media.length + newFiles.length > 0 ? 'Vai como legenda do primeiro anexo · ' : ''}
                Ctrl+Enter para {mode === 'edit' ? 'salvar' : 'adicionar'}
              </span>
            </div>

            <div className="mc-qrp-field">
              <label className="mc-qrp-label">Anexos</label>
              {(media.length > 0 || newFiles.length > 0) && (
                <div className="mc-qrp-files">
                  {media.map(m => (
                    <div key={m.path} className="mc-qrp-file">
                      <MIcon name={{ image: 'img', video: 'video', audio: 'mic' }[m.mediaType] || 'doc'} size={14} />
                      <span className="mc-qrp-file-name">{m.fileName}</span>
                      <button className="mc-qrp-file-x" title="Remover"
                        onClick={() => { setMedia(prev => prev.filter(x => x.path !== m.path)); setRemoved(prev => [...prev, m.path]); }}>
                        <MIcon name="xmark" size={11} />
                      </button>
                    </div>
                  ))}
                  {newFiles.map((f, i) => (
                    <div key={'n' + i} className="mc-qrp-file new">
                      <MIcon name={{ image: 'img', video: 'video', audio: 'mic' }[_qrMediaType(f.type)] || 'doc'} size={14} />
                      <span className="mc-qrp-file-name">{f.name}</span>
                      <button className="mc-qrp-file-x" title="Remover"
                        onClick={() => setNewFiles(prev => prev.filter((_, j) => j !== i))}>
                        <MIcon name="xmark" size={11} />
                      </button>
                    </div>
                  ))}
                </div>
              )}
              <input ref={fileInRef} type="file" multiple style={{ display: 'none' }} onChange={onPickFiles} />
              <button type="button" className="mc-qrp-attach-btn" onClick={() => fileInRef.current?.click()}>
                <MIcon name="clip" size={14} />
                Foto, vídeo, áudio ou arquivo
              </button>
            </div>

            {err && <div className="mc-qrp-err">{err}</div>}

            <div style={{ display: 'flex', gap: 8 }}>
              {mode === 'edit' && (
                <button className="mc-qrp-cancel-btn" onClick={cancelEdit}>Cancelar</button>
              )}
              <button className="mc-qrp-add-btn" style={{ flex: 1, opacity: saving ? 0.6 : 1 }} disabled={saving}
                onClick={mode === 'edit' ? saveEdit : addReply}>
                {saving ? 'Salvando…' : mode === 'edit' ? (
                  <>
                    <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
                      <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
                      <polyline points="17 21 17 13 7 13 7 21"/>
                      <polyline points="7 3 7 8 15 8"/>
                    </svg>
                    Salvar alterações
                  </>
                ) : (
                  <>
                    <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"><path d="M12 5v14M5 12h14"/></svg>
                    Adicionar resposta
                  </>
                )}
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ── Busca de conversas ───────────────────────────────────────────────────── */
const _searchNorm = s => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
// Telefone → chaves sem 55, com e sem o 9º dígito
function _searchPhoneKeys(raw) {
  let d = String(raw || '').split('@')[0].replace(/\D/g, '');
  if ((d.length === 12 || d.length === 13) && d.startsWith('55')) d = d.slice(2);
  if (d.length < 10 || d.length > 11) return [];
  const keys = [d];
  if (d.length === 11 && d[2] === '9') keys.push(d.slice(0, 2) + d.slice(3));
  if (d.length === 10) keys.push(d.slice(0, 2) + '9' + d.slice(2));
  return keys;
}
function _convMatches(c, search, leadKeys) {
  const q = _searchNorm(search).trim();
  if (!q) return true;
  const name = _searchNorm(c.name);
  if (q.split(/\s+/).every(w => name.includes(w))) return true;
  let digits = search.replace(/\D/g, '');
  if (digits.length >= 12 && digits.startsWith('55')) digits = digits.slice(2);
  const phone = String(c.phone || '').replace(/\D/g, '');
  if (digits.length >= 3 && phone) {
    if (phone.includes(digits)) return true;
    // sem o 9º dígito de um lado ou do outro
    if (digits.length >= 8 && _searchPhoneKeys(phone).some(k => k.includes(digits) || digits.includes(k))) return true;
  }
  return !!(leadKeys && leadKeys.size && _searchPhoneKeys(c.phone || c.jid).some(k => leadKeys.has(k)));
}

/* ── ConvList ────────────────────────────────────────────────────────────── */

function _ConvList({ convs, leads, selected, filter, search, waLabels, labelVisible,
  onSelect, onFilter, onSearch, onNewConv, onSync, isSyncing, onDisconnect,
  onSelectReply, onPin, onArchive, onMarkUnread, onResolve, convsLoading, readConvIds, onBulkLabel, onAddConv, onLidLink }) {

  // Seleção em massa (para etiquetar várias conversas de uma vez)
  const [selMode, setSelMode]   = React.useState(false);
  const [selIds, setSelIds]     = React.useState(() => new Set());
  const [selMenu, setSelMenu]   = React.useState(false);
  const [selRun, setSelRun]     = React.useState(null);   // { done, total }
  const [selMsg, setSelMsg]     = React.useState('');
  const selMenuRef              = React.useRef();
  const [selAsk, setSelAsk]     = React.useState(null);   // { label, add, n } — confirmação antes de aplicar
  React.useEffect(() => { if (!selMenu) setSelAsk(null); }, [selMenu]);
  const exitSel = () => { setSelMode(false); setSelIds(new Set()); setSelMenu(false); };
  React.useEffect(() => { if (!onBulkLabel && selMode) exitSel(); }, [onBulkLabel, selMode]);

  const [menuOpen, setMenuOpen]             = React.useState(false);
  const [showQR, setShowQR]                 = React.useState(false);
  const [subFilter, setSubFilter]           = React.useState('atendendo');
  const [labelDropOpen, setLabelDropOpen]   = React.useState(false);
  const [configDropOpen, setConfigDropOpen] = React.useState(false);
  const menuRef                             = React.useRef();
  const labelDropRef                        = React.useRef();
  const configDropRef                       = React.useRef();

  React.useEffect(() => {
    if (!menuOpen) return;
    function h(e) { if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false); }
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [menuOpen]);

  React.useEffect(() => {
    if (!labelDropOpen) return;
    function h(e) { if (labelDropRef.current && !labelDropRef.current.contains(e.target)) setLabelDropOpen(false); }
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [labelDropOpen]);

  React.useEffect(() => {
    if (!configDropOpen) return;
    function h(e) { if (configDropRef.current && !configDropRef.current.contains(e.target)) setConfigDropOpen(false); }
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [configDropOpen]);

  // ── Keyboard shortcuts (C, ⌘K, Esc) ──────────────────────────────────────
  React.useEffect(() => {
    function h(e) {
      const tag = document.activeElement?.tagName;
      const inInput = tag === 'INPUT' || tag === 'TEXTAREA' || document.activeElement?.isContentEditable;

      // C → nova conversa (não dispara dentro de inputs)
      if (e.key === 'c' && !inInput && !e.ctrlKey && !e.metaKey && !e.altKey) {
        e.preventDefault();
        onNewConv();
        return;
      }

      // ⌘K / Ctrl+K → buscar
      if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        onFilter('search');
        return;
      }

      // Esc → fechar busca
      if (e.key === 'Escape') {
        if (filter === 'search') { onFilter('all'); onSearch(''); }
      }
    }
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, [filter, onNewConv, onFilter, onSearch]);

  React.useEffect(() => {
    if (!selMenu) return;
    function h(e) { if (selMenuRef.current && !selMenuRef.current.contains(e.target)) setSelMenu(false); }
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, [selMenu]);

  const leadMap = React.useMemo(() => {
    const m = {};
    for (const l of leads) {
      const d = (l.phone || '').replace(/\D/g, '');
      if (d) m[d] = { temp: l.temp, score: l.score };
    }
    return m;
  }, [leads]);

  // Conversas fantasma: LID sem nome real, sem mensagem, sem não lidas — omitir da lista
  const validConvs = React.useMemo(() =>
    convs.filter(c => !c.unnamed || c.last || c.unread > 0 || c.labels.length > 0),
  [convs]);

  // Set de telefones de leads (dígitos sem prefixo 55) para filtrar subtabs CRM
  const leadPhoneSet = React.useMemo(() => {
    const s = new Set();
    for (const l of leads) {
      const d = (l.phone || '').replace(/\D/g, '');
      if (!d) continue;
      const norm = d.startsWith('55') && d.length >= 12 ? d.slice(2) : d;
      s.add(norm);
      s.add(d); // também adiciona com prefixo, para cobrir ambos os formatos
      // com e sem o 9º dígito (WhatsApp às vezes guarda o celular sem ele)
      if (norm.length === 11 && norm[2] === '9') s.add(norm.slice(0, 2) + norm.slice(3));
      else if (norm.length === 10) s.add(norm.slice(0, 2) + '9' + norm.slice(2));
    }
    return s;
  }, [leads]);

  function _convInCrm(c) {
    const raw = (c.phone || c.jid?.split('@')[0] || '').replace(/\D/g, '');
    const norm = raw.startsWith('55') && raw.length >= 12 ? raw.slice(2) : raw;
    return leadPhoneSet.has(norm) || leadPhoneSet.has(raw);
  }

  // Busca: a lista só tem as 200 conversas mais recentes — procura também no banco (nome, número,
  // e nome do lead do CRM) e junta os resultados. Sem acento/maiúscula; número com ou sem máscara/55.
  const [searchHits, setSearchHits]         = React.useState([]);
  const [searchLeadKeys, setSearchLeadKeys] = React.useState(() => new Set());
  const [searching, setSearching]           = React.useState(false);
  React.useEffect(() => {
    const q = search.trim();
    if (q.length < 2 || !window.HOROS_SB) { setSearchHits([]); setSearchLeadKeys(new Set()); setSearching(false); return; }
    let alive = true;
    setSearching(true);
    const t = setTimeout(async () => {
      const sb = window.HOROS_SB;
      const uid = window.horosActUid?.() || window.HOROS_USER?.id;
      const txt = q.replace(/[,()*%\\]/g, ' ').trim();
      let digits = q.replace(/\D/g, '');
      if (digits.length >= 12 && digits.startsWith('55')) digits = digits.slice(2);
      const isNum = digits.length >= 4 && digits.length >= q.replace(/\s/g, '').length - 4;
      try {
        // leads do CRM com esse nome/número → telefones (com/sem 55 e 9º dígito)
        let lq = sb.from('leads').select('phone').eq('archived', false).limit(40);
        lq = isNum ? lq.ilike('phone', `%${digits.slice(-8)}%`) : lq.ilike('name', `%${txt}%`);
        const ors = [];
        if (txt) ors.push(`contact_name.ilike.%${txt}%`);
        if (isNum) ors.push(`contact_phone.ilike.%${digits.slice(-8)}%`, `wa_jid.ilike.%${digits.slice(-8)}%`);
        let cq = sb.from('conversations').select('*').not('wa_jid', 'like', '%status@broadcast%')
          .order('last_message_at', { ascending: false, nullsFirst: false }).limit(60);
        if (uid) cq = cq.eq('user_id', uid);
        if (ors.length) cq = cq.or(ors.join(','));
        const [{ data: lds }, { data: cvs }] = await Promise.all([lq, cq]);
        const keys = new Set();
        (lds || []).forEach(l => _searchPhoneKeys(l.phone).forEach(k => keys.add(k)));
        let rows = cvs || [];
        // conversas dos leads achados que não vieram pelo nome do contato
        const tails = [...new Set([...keys].map(k => k.slice(-8)))].slice(0, 20);
        if (tails.length) {
          let q2 = sb.from('conversations').select('*')
            .or(tails.map(t => `contact_phone.ilike.%${t}`).join(',')).limit(60);
          if (uid) q2 = q2.eq('user_id', uid);
          const { data: more } = await q2;
          rows = rows.concat(more || []);
        }
        if (!alive) return;
        setSearchLeadKeys(keys);
        setSearchHits(rows.map(_mapConv));
      } catch (_) {
        if (alive) setSearchHits([]);
      } finally {
        if (alive) setSearching(false);
      }
    }, 300);
    return () => { alive = false; clearTimeout(t); };
  }, [search]);

  const filtered = React.useMemo(() => {
    let list = [...validConvs];
    if (filter === 'archived') list = list.filter(c => c.archived);
    else {
      list = list.filter(c => !c.archived);
      if (filter === 'pending' || filter === 'unread') list = list.filter(c => c.unread > 0);
      else if (filter.startsWith('label:')) {
        const id  = filter.slice(6);
        const sys = _waSystemList(waLabels.find(l => l.label_id === id));
        list = sys === 'unread' ? list.filter(c => c.unread > 0)
             : sys === 'groups' ? list.filter(c => c.jid.endsWith('@g.us'))
             : list.filter(c => c.labels.includes(id));
      }
      // sub-tab: 'aguardando' = não lidas (aguardando resposta) e só de contatos que estão no CRM
      if (subFilter === 'aguardando') list = list.filter(c => c.unread > 0 && _convInCrm(c));
    }
    // Busca só vale na aba Buscar — texto que sobrou de lá (campo escondido) não pode
    // tomar o lugar do filtro de etiqueta (todas as etiquetas mostravam a mesma lista)
    if (filter === 'search' && search.trim()) {
      // Busca vale para todas as conversas (inclusive arquivadas e fora da aba atual) + as do banco
      const seen = new Set();
      list = [...validConvs, ...searchHits].filter(c => !seen.has(c.id) && seen.add(c.id) && _convMatches(c, search, searchLeadKeys));
    }
    return list.sort((a, b) => {
      if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
      return new Date(b.ts) - new Date(a.ts);
    });
  }, [validConvs, filter, subFilter, search, leadPhoneSet, waLabels, searchHits, searchLeadKeys]);

  // Desenha a lista aos poucos (2026-10-11, "o site parece estar pesado"): com ~5 mil conversas
  // a lista inteira eram 65 mil elementos na página e cada mensagem/tecla redesenhava tudo.
  // Começa com 60 e acrescenta 120 quando o fim da lista chega perto da vista.
  const LIST_STEP = 120;
  const [shownCount, setShownCount] = React.useState(60);
  React.useEffect(() => { setShownCount(60); }, [filter, subFilter, search]);
  const onListScroll = e => {
    const el = e.currentTarget;
    if (shownCount < filtered.length && el.scrollHeight - el.scrollTop - el.clientHeight < 900) {
      setShownCount(n => n + LIST_STEP);
    }
  };
  const visibleConvs = React.useMemo(() => {
    const list = filtered.slice(0, shownCount);
    // conversa aberta fora do pedaço desenhado continua marcada na lista
    if (selected && !list.some(c => c.id === selected)) {
      const sel = filtered.find(c => c.id === selected);
      if (sel) list.push(sel);
    }
    return list;
  }, [filtered, shownCount, selected]);

  const counts = React.useMemo(() => {
    const active = validConvs.filter(c => !c.archived);
    const crmActive = active.filter(_convInCrm);
    const m = {
      all:     crmActive.length,
      pending: crmActive.filter(c => c.unread > 0).length,
      unread:  active.filter(c => c.unread > 0).length,
    };
    for (const l of waLabels) {
      const sys = _waSystemList(l);
      m[`label:${l.label_id}`] = sys === 'unread' ? m.unread
        : sys === 'groups' ? active.filter(c => c.jid.endsWith('@g.us')).length
        : active.filter(c => c.labels.includes(l.label_id)).length;
    }
    return m;
  }, [convs, waLabels, leadPhoneSet]);

  // Modo seleção: Esc sai · Ctrl/Cmd+A marca todas as conversas da lista atual
  React.useEffect(() => {
    if (!selMode) return;
    function h(e) {
      const tag = document.activeElement?.tagName;
      if (tag === 'INPUT' || tag === 'TEXTAREA' || document.activeElement?.isContentEditable) return;
      if (e.key === 'Escape') { e.preventDefault(); if (selMenu) setSelMenu(false); else exitSel(); }
      else if ((e.key === 'a' || e.key === 'A') && (e.ctrlKey || e.metaKey)) {
        e.preventDefault();
        setSelIds(new Set(filtered.map(c => c.id)));
      }
    }
    document.addEventListener('keydown', h, true);
    return () => document.removeEventListener('keydown', h, true);
  }, [selMode, selMenu, filtered]);

  const toggleSel = (id) => setSelIds(prev => {
    const n = new Set(prev);
    n.has(id) ? n.delete(id) : n.add(id);
    return n;
  });
  const selConvs  = selMode ? convs.filter(c => selIds.has(c.id)) : [];
  const tagLabels = _waTagLabels(waLabels);
  const runBulkLabel = async (label, add) => {
    setSelMenu(false); setSelMsg('');
    const r = await onBulkLabel(selConvs, label.label_id, add, (done, total) => setSelRun({ done, total }));
    setSelRun(null);
    setSelMsg(r.total === 0
      ? (add ? `Todas já têm "${label.name}".` : `Nenhuma tem "${label.name}".`)
      : `"${label.name}" ${add ? 'colocada em' : 'tirada de'} ${r.ok} de ${r.total}` + (r.fail ? ` · ${r.fail} falharam` : ''));
    setTimeout(() => setSelMsg(''), 6000);
  };

  const visibleLabels = waLabels.filter(l => labelVisible[l.label_id] !== false);
  // "Favoritos" do WhatsApp não chega pela Evolution (Baileys ignora FavoritesAction) — fora do menu
  const menuLabels = waLabels.filter(l => _waSystemList(l) !== 'favorites');

  return (
    <div className="mc-list">
      {/* Pill filter bar — Abertos / Resolvidos / Buscar */}
      <div className="mc-pill-bar">
        <button className={`mc-pill-btn${filter === 'all' ? ' active' : ''}`} title="Abertos"
          onClick={() => { onFilter('all'); onSearch(''); }}>
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
          </svg>
          <span className="mc-pill-lbl">Abertos</span>
        </button>
        <button className={`mc-pill-btn${filter === 'archived' ? ' active' : ''}`} title="Arquivadas"
          onClick={() => { onFilter('archived'); onSearch(''); }}>
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <rect x="2" y="4" width="20" height="5" rx="2"/><path d="M4 9v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9M10 13h4"/>
          </svg>
          <span className="mc-pill-lbl">Arquivadas</span>
        </button>
        <button className={`mc-pill-btn${filter === 'search' ? ' active' : ''}`} title="Buscar"
          onClick={() => onFilter('search')}>
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <circle cx="11" cy="11" r="8"/>
            <line x1="21" y1="21" x2="16.65" y2="16.65"/>
          </svg>
          <span className="mc-pill-lbl">Buscar</span>
        </button>
      </div>

      {/* Sub-tabs — Atendendo / Aguardando */}
      <div className="mc-subtab-bar">
        <button className={`mc-subtab${subFilter === 'atendendo' ? ' active' : ''}`}
          onClick={() => setSubFilter('atendendo')}>
          Atendendo
          {counts.all > 0 && <span className="mc-subtab-badge purple">{counts.all}</span>}
        </button>
        <button className={`mc-subtab${subFilter === 'aguardando' ? ' active' : ''}`}
          onClick={() => setSubFilter('aguardando')}>
          Aguardando
          {counts.pending > 0 && <span className="mc-subtab-badge orange">{counts.pending}</span>}
        </button>
      </div>

      {/* Action pills — Configurações / Novo contato / Etiquetas */}
      <div className="mc-action-pills">
        {/* Configurações com dropdown */}
        <div className="mc-action-pill-wrap mc-cfg-wrap" ref={configDropRef}>
          <button className="mc-action-pill" title="Configurações" onClick={() => setConfigDropOpen(v => !v)}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="12" cy="12" r="3"/>
              <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
            </svg>
            <span className="mc-pill-lbl">Configurações</span>
            <svg className="mc-pill-caret" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
              style={{ transition: 'transform 0.15s', transform: configDropOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}>
              <polyline points="6 9 12 15 18 9"/>
            </svg>
          </button>
          {configDropOpen && (
            <div className="mc-label-drop" style={{ left: 0, right: 'auto' }}>
              <button className="mc-label-drop-item" disabled={isSyncing}
                onClick={() => { onSync(); setConfigDropOpen(false); }}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
                  style={isSyncing ? { animation: 'spin 1s linear infinite' } : {}}>
                  <polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/>
                  <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
                </svg>
                Sincronizar
              </button>
              <div className="mc-head-dropdown-sep" style={{ margin: '3px 10px' }}/>
              <button className="mc-label-drop-item"
                onClick={() => { setShowQR(true); setConfigDropOpen(false); }}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
                  <line x1="9" y1="10" x2="9" y2="10"/><line x1="12" y1="10" x2="12" y2="10"/><line x1="15" y1="10" x2="15" y2="10"/>
                </svg>
                Mensagens rápidas
              </button>
              <button className="mc-label-drop-item"
                onClick={() => { setConfigDropOpen(false); window.location.href = 'broadcast.html'; }}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/>
              <path d="m12 15-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/>
              <path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>
                </svg>
                Disparo em massa
              </button>
              <button className="mc-label-drop-item"
                onClick={() => { setConfigDropOpen(false); window.dispatchEvent(new CustomEvent('horos:open-auto-assistant')); }}>
                <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                  <rect x="4" y="7" width="16" height="12" rx="3" fill="currentColor" fillOpacity={0.18}/><path d="M12 7V4M9 12h.01M15 12h.01M9.5 15.5h5"/>
                </svg>
                Assistente automático <HorosMaxTag />
              </button>
              {onDisconnect && (
                <>
                  <div className="mc-head-dropdown-sep" style={{ margin: '3px 10px' }}/>
                  <button className="mc-label-drop-item" style={{ color: '#ef4444' }}
                    onClick={() => { onDisconnect(); setConfigDropOpen(false); }}>
                    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                      <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
                      <polyline points="16 17 21 12 16 7"/>
                      <line x1="21" y1="12" x2="9" y2="12"/>
                    </svg>
                    Desconectar
                  </button>
                </>
              )}
            </div>
          )}
        </div>

        <button className="mc-action-pill mc-new-pill" title="Novo contato" onClick={onNewConv}>
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
            <line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
          </svg>
          <span className="mc-pill-lbl">Novo contato</span>
        </button>

        {/* Etiquetas com dropdown */}
        <div className="mc-action-pill-wrap" ref={labelDropRef}>
          <button className={`mc-action-pill${filter.startsWith('label:') ? ' active' : ''}`} title="Etiquetas"
            onClick={() => setLabelDropOpen(v => !v)}>
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/>
            </svg>
            <span className="mc-pill-lbl">Etiquetas</span>
            <svg className="mc-pill-caret" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"
              style={{ transition: 'transform 0.15s', transform: labelDropOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}>
              <polyline points="6 9 12 15 18 9"/>
            </svg>
          </button>
          {labelDropOpen && (
            <div className="mc-label-drop">
              {filter.startsWith('label:') && (
                <button className="mc-label-drop-item"
                  onClick={() => { onFilter('all'); onSearch(''); setLabelDropOpen(false); }}>
                  <span className="mc-label-drop-dot" style={{ background: '#cccccc' }}/>
                  Todas
                </button>
              )}
              {menuLabels.length === 0
                ? <div className="mc-label-drop-empty">Nenhuma etiqueta</div>
                : menuLabels.map((l, i) => (
                  <button key={l.label_id}
                    className={`mc-label-drop-item${filter === 'label:' + l.label_id ? ' sel' : ''}`}
                    onClick={() => { onFilter('label:' + l.label_id); onSearch(''); setLabelDropOpen(false); }}>
                    <span className="mc-label-drop-dot" style={{ background: _waLabelDot(l) }}/>
                    {l.name}
                    {(counts['label:' + l.label_id] || 0) > 0 && (
                      <span className="mc-label-drop-cnt">{counts['label:' + l.label_id]}</span>
                    )}
                  </button>
                ))
              }
            </div>
          )}
        </div>

        {onBulkLabel && (
          <button className={`mc-action-pill mc-sel-pill${selMode ? ' active' : ''}`} title={selMode ? 'Cancelar seleção (Esc)' : 'Selecionar conversas para etiquetar'}
            aria-label={selMode ? 'Cancelar seleção' : 'Selecionar conversas'} onClick={() => selMode ? exitSel() : setSelMode(true)}>
            {selMode
              ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              : <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="4"/><polyline points="8 12 11 15 16 9"/></svg>}
          </button>
        )}
      </div>

      {/* Barra de busca — visível apenas na tab Buscar */}
      {filter === 'search' && (
        <div className="mc-search-bar-wrap">
          <div className="mc-search">
            <Icon name="search" size={13} className="ico" />
            <input value={search} onChange={e => onSearch(e.target.value)}
              placeholder="Buscar conversas…" autoFocus />
            {search && (
              <button style={{ color: 'var(--fg-4)', padding: 2 }} onClick={() => onSearch('')}>
                <MIcon name="xmark" size={13} />
              </button>
            )}
          </div>
        </div>
      )}


      <div className="mc-list-items" onScroll={onListScroll}>
        {filtered.length === 0 && convsLoading && (
          <SkelList rows={9} avatar={44} right pad="10px 16px" label="Carregando conversas" />
        )}
        {filtered.length === 0 && !convsLoading && (
          <div style={{ padding: 32, textAlign: 'center', color: 'var(--fg-4)', fontSize: 13 }}>
            {search.trim() ? (searching ? 'Buscando…' : 'Nenhum resultado') : 'Nenhuma conversa'}
          </div>
        )}

        {/* Lista plana ordenada por última mensagem — badge de não-lidas mantido por _ConvItem.isUnread */}
        {visibleConvs.map(c => {
          const d = (c.phone || '').replace(/\D/g, '');
          const info = leadMap[d] || leadMap[d.replace(/^55/, '')] || null;
          return (
            <_ConvItem key={c.id} conv={c} selected={c.id === selected}
              leadTemp={info?.temp || null} lead={info} waLabels={waLabels}
              onClick={() => {
                if (selMode) return toggleSel(c.id);
                // resultado da busca que não estava entre as conversas carregadas: entra na lista antes
                if (onAddConv && !convs.some(x => x.id === c.id)) { onAddConv(c); setTimeout(() => onSelect(c.id), 30); return; }
                onSelect(c.id);
              }}
              selectMode={selMode} checked={selMode && selIds.has(c.id)}
              onLidLink={onLidLink}
              onPin={onPin} onArchive={onArchive} onMarkUnread={onMarkUnread} onResolve={onResolve}
              readConvIds={readConvIds} />
          );
        })}
      </div>

      {selMode && (
        <div className="mc-sel-bar">
          {selMsg && !selRun && <div className="mc-sel-toast" onClick={() => setSelMsg('')}>{selMsg}</div>}
          <div className="mc-sel-bar-row">
            <span className="mc-sel-count"><b>{selIds.size}</b> {selIds.size === 1 ? 'selecionada' : 'selecionadas'}</span>
            {selIds.size < filtered.length
              ? <button className="mc-sel-link" onClick={() => setSelIds(new Set(filtered.map(c => c.id)))}>Todas ({filtered.length})</button>
              : <button className="mc-sel-link" onClick={() => setSelIds(new Set())}>Desmarcar</button>}
            <div className="mc-sel-label-wrap" ref={selMenuRef}>
              <button className={`mc-sel-btn primary${selMenu ? ' on' : ''}`} disabled={!selIds.size || !!selRun || !tagLabels.length}
                onClick={() => setSelMenu(v => !v)}>
                {selRun
                  ? <><span className="mc-sel-spin" /> {selRun.done}/{selRun.total}</>
                  : <><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg> Etiquetar</>}
              </button>
              {selMenu && (
                <div className="mc-sel-pop">
                  {selAsk ? (
                    <div className="mc-sel-ask">
                      <div className="mc-sel-ask-title">
                        {selAsk.add ? 'Colocar' : 'Excluir'} <span style={{ color: _waLabelDot(selAsk.label) }}>{selAsk.label.name}</span> {selAsk.add ? 'em' : 'de'} {selAsk.n} {selAsk.n === 1 ? 'conversa' : 'conversas'}?
                      </div>
                      <div className="mc-sel-ask-text">
                        {selAsk.add ? 'A etiqueta é colocada no WhatsApp de cada contato.' : 'A etiqueta é tirada do WhatsApp de cada contato.'}
                      </div>
                      <div className="mc-sel-ask-btns">
                        <button className="mc-sel-btn" onClick={() => setSelAsk(null)}>Cancelar</button>
                        <button className={`mc-sel-btn ${selAsk.add ? 'primary' : 'danger'}`} onClick={() => runBulkLabel(selAsk.label, selAsk.add)}>
                          {selAsk.add ? 'Confirmar' : 'Excluir'}
                        </button>
                      </div>
                    </div>
                  ) : <>
                  <div className="mc-sel-pop-head">Etiqueta em {selIds.size} {selIds.size === 1 ? 'conversa' : 'conversas'}</div>
                  <div className="mc-sel-pop-list">
                    {tagLabels.map(l => {
                      const n = selConvs.filter(c => (c.labels || []).includes(l.label_id)).length;
                      const all = n === selConvs.length && n > 0;
                      return (
                        <button key={l.label_id} className="mc-sel-pop-item" onClick={() => setSelAsk({ label: l, add: !all, n: all ? n : selConvs.length - n })}
                          title={all ? 'Clique para tirar de todas' : 'Clique para colocar em todas'}>
                          <span className="mc-label-drop-dot" style={{ background: _waLabelDot(l) }}/>
                          <span className="mc-sel-pop-name">{l.name}</span>
                          <span className="mc-sel-pop-meta">
                            {all
                              ? <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round"><polyline points="20 6 9 17 4 12"/></svg>
                              : n > 0 ? `${n}/${selConvs.length}` : ''}
                          </span>
                        </button>
                      );
                    })}
                  </div>
                  <div className="mc-sel-pop-note">Já está em todas → clique tira. Senão, coloca em quem falta.</div>
                  </>}
                </div>
              )}
            </div>
            <button className="mc-sel-btn" onClick={exitSel} disabled={!!selRun} title="Sair do modo seleção (Esc)">Concluir</button>
          </div>
        </div>
      )}

      {/* Shortcuts bar */}
      {!selMode && <div className="mc-shortcuts">
        <span><b>C</b> nova conversa</span>
        <span><b>⌘K</b> buscar</span>
        <span><b>R</b> responder</span>
        <span><b>Esc</b> fechar</span>
      </div>}

      {showQR && <_QuickRepliesPanel onClose={() => setShowQR(false)} onSelectReply={onSelectReply} />}
    </div>
  );
}


/* ── MessagesPage ────────────────────────────────────────────────────────── */

// Apresentação do copiloto de IA — aparece uma vez em Mensagens (veio do onboarding, pedido do usuário 2026-10-06)
function _CopilotIntroModal({ onClose }) {
  const plan = useHorosPlan();
  React.useEffect(() => {
    const onKey = e => { if (e.key === 'Escape') onClose(false); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);
  return (
    <div className="mc-cpi-ov" onMouseDown={e => { if (e.target === e.currentTarget) onClose(false); }}>
      <div className="mc-cpi" role="dialog" aria-modal="true" aria-labelledby="mc-cpi-t">
        <div className="mc-cpi-h" id="mc-cpi-t">Respostas prontas no WhatsApp, automaticamente.</div>
        <div className="mc-cpi-b">
          <p>Quando um cliente escreve no WhatsApp, a IA do Horos lê a conversa e sugere a resposta no seu jeito de escrever. Você aprova, edita ou descarta — nada é enviado sem você.</p>
          {plan.tier === 'pro' && !plan.pending && (
            <p className="mc-cpi-trial">No seu plano você tem <b>{Math.max(0, (plan.copilot_trial || 50) - (plan.copilot_used || 0))} sugestões</b> para testar. O copiloto sem limite faz parte do plano Max.</p>
          )}
          <div className="mc-cpi-ill">
            <div className="mc-cpi-art">
              <div className="mc-cpi-win">
                <div className="mc-cpi-top"><img src="favicon.svg" alt="" />Mensagens</div>
                <div className="mc-cpi-chat">
                  <div className="mc-cpi-bub">Oi! Vi o anúncio. Ainda tem disponível? Qual a entrada?</div>
                  <div className="mc-cpi-sug">
                    <b><Icon name="sparkles" size={12} />Resposta sugerida</b>
                    Tem sim! A entrada começa em 10%. Posso te mandar a simulação com as parcelas? Qual o melhor horário para conversarmos?
                    <div className="mc-cpi-acts"><i>Enviar</i><i>Editar</i></div>
                  </div>
                </div>
              </div>
              <div className="mc-cpi-tag" style={{ left: '7%', top: 118, animationDelay: '.35s' }}>+ Temperatura</div>
              <div className="mc-cpi-tag" style={{ left: '3%', top: 162, animationDelay: '.55s' }}>+ Próximo passo</div>
              <div className="mc-cpi-tag" style={{ left: '9%', top: 206, animationDelay: '.75s' }}>+ Etapa do funil</div>
            </div>
            <div className="mc-cpi-cap">
              <b>Atendimento rápido, sem digitar tudo</b>
              <p>O copiloto também marca a temperatura do lead e sugere o próximo passo, para nenhum cliente ficar esperando. Dá para desligar quando quiser, no botão ao lado do campo de mensagem.</p>
            </div>
          </div>
        </div>
        <div className="mc-cpi-f">
          <button className="mc-cpi-skip" onClick={() => onClose(false)}>Ignorar por enquanto</button>
          <button className="mc-cpi-ok" onClick={() => onClose(true)}>Ativar copiloto</button>
        </div>
      </div>
    </div>
  );
}

// ── WhatsApp oficial (Cloud API): janela de 24 h + envio de modelo aprovado ──────────────
// Pela API oficial, passadas 24 h da última mensagem do cliente só dá para mandar modelo aprovado pelo Meta.
const _CLOUD_WINDOW_MS = 24 * 3600 * 1000;
function _cloudWindowOpen(conv) {
  return !!conv?.lastInboundAt && (Date.now() - new Date(conv.lastInboundAt).getTime()) < _CLOUD_WINDOW_MS;
}
function _cloudWindowLeft(conv) {
  if (!_cloudWindowOpen(conv)) return null;
  const ms = _CLOUD_WINDOW_MS - (Date.now() - new Date(conv.lastInboundAt).getTime());
  const h = Math.floor(ms / 3600000), m = Math.floor((ms % 3600000) / 60000);
  return h > 0 ? `${h} h` : `${m} min`;
}

function _CloudWindowBar({ onOpen, isGroup }) {
  return (
    <div className="mc-cw-bar">
      <div className="mc-cw-ic"><MIcon name="alert" size={16} /></div>
      <div className="mc-cw-txt">
        {isGroup
          ? <><b>A API oficial não envia para grupos.</b> As mensagens do grupo aparecem aqui só para leitura.</>
          : <><b>Passaram mais de 24 h desde a última mensagem do cliente.</b> Pelo WhatsApp oficial, só dá para enviar um modelo aprovado pelo Meta. Quando ele responder, a conversa volta a ficar livre.</>}
      </div>
      {!isGroup && <button className="mc-cw-btn" onClick={onOpen}>Enviar modelo</button>}
    </div>
  );
}

function _tplVarCount(body) {
  let max = 0;
  String(body || '').replace(/\{\{\s*(\d+)\s*\}\}/g, (_, n) => { max = Math.max(max, Number(n)); return _; });
  return max;
}
function _tplFill(body, params) {
  return String(body || '').replace(/\{\{\s*(\d+)\s*\}\}/g, (m, n) => (params[Number(n) - 1] || '').trim() || m);
}

function _CloudTemplateModal({ conv, onClose, onSent }) {
  const [list, setList]   = React.useState(null);
  const [err, setErr]     = React.useState('');
  const [manage, setManage] = React.useState('');
  const [sel, setSel]     = React.useState(null);
  const [params, setParams] = React.useState([]);
  const [q, setQ]         = React.useState('');
  const [sending, setSending] = React.useState(false);
  const first = String(conv?.name || '').trim().split(/\s+/)[0] || '';

  React.useEffect(() => {
    (async () => {
      try {
        const tok = window.horosToken ? await window.horosToken() : window.HOROS_SESSION?.access_token;
        const r = await fetch('/api/whatsapp-cloud?templates=1', { headers: { Authorization: `Bearer ${tok}` }, cache: 'no-store' });
        const d = await r.json().catch(() => ({}));
        if (!r.ok) throw new Error(d.error || 'Não foi possível carregar os modelos.');
        setList(d.templates || []); setManage(d.manageUrl || '');
      } catch (e) { setErr(e.message); setList([]); }
    })();
  }, []);
  React.useEffect(() => {
    const k = e => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', k);
    return () => window.removeEventListener('keydown', k);
  }, [onClose]);

  const pick = (t) => {
    setSel(t); setErr('');
    const n = _tplVarCount(t.body);
    setParams(Array.from({ length: n }, (_, i) => (i === 0 && /^[A-Za-zÀ-ÿ]/.test(first) ? first : '')));
  };
  const missing = sel ? params.some(p => !String(p).trim()) : true;
  const send = async () => {
    if (!sel || missing || sending) return;
    setSending(true); setErr('');
    try {
      const tok = window.horosToken ? await window.horosToken() : window.HOROS_SESSION?.access_token;
      const preview = _tplFill(sel.body, params);
      const r = await fetch('/api/whatsapp-cloud', {
        method: 'POST', headers: { Authorization: `Bearer ${tok}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ action: 'template', conversationId: conv.id, name: sel.name, language: sel.language, params, preview }),
      });
      const d = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(d.error || 'O WhatsApp não aceitou o modelo.');
      onSent && onSent({ id: d.messageId, body: d.body || preview });
      onClose();
    } catch (e) { setErr(e.message); setSending(false); }
  };

  const norm = s => String(s || '').normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
  const shown = (list || []).filter(t => !q || norm(t.name + ' ' + t.body).includes(norm(q)));
  const catLabel = c => ({ MARKETING: 'Marketing', UTILITY: 'Utilidade', AUTHENTICATION: 'Autenticação' })[c] || c;

  return ReactDOM.createPortal(
    <div className="mc-tpl-ov" onClick={onClose}>
      <div className="mc-tpl" role="dialog" aria-label="Enviar modelo" onClick={e => e.stopPropagation()}>
        <div className="mc-tpl-h">
          <div>
            <div className="mc-tpl-title">Enviar modelo</div>
            <div className="mc-tpl-sub">Para {conv?.name || 'o contato'} · modelos aprovados pelo Meta</div>
          </div>
          <button className="mc-tpl-x" onClick={onClose} aria-label="Fechar"><MIcon name="xmark" size={18} /></button>
        </div>
        <div className="mc-tpl-body">
          <div className="mc-tpl-list">
            <input className="mc-tpl-search" placeholder="Buscar modelo" value={q} onChange={e => setQ(e.target.value)} />
            {list === null && <div className="mc-tpl-empty">Carregando modelos…</div>}
            {list !== null && !shown.length && (
              <div className="mc-tpl-empty">
                {list.length ? 'Nenhum modelo com esse nome.' : 'Nenhum modelo aprovado ainda.'}
                {manage && <a href={manage} target="_blank" rel="noopener noreferrer">Criar modelo no WhatsApp Manager</a>}
              </div>
            )}
            {shown.map(t => (
              <button key={t.name + t.language} className={'mc-tpl-item' + (sel && sel.name === t.name && sel.language === t.language ? ' on' : '')} onClick={() => pick(t)}>
                <div className="mc-tpl-item-top"><b>{t.name}</b><span>{catLabel(t.category)} · {t.language}</span></div>
                <div className="mc-tpl-item-body">{t.body}</div>
              </button>
            ))}
          </div>
          <div className="mc-tpl-side">
            {!sel ? <div className="mc-tpl-empty">Escolha um modelo para ver a prévia.</div> : (
              <>
                {params.length > 0 && (
                  <div className="mc-tpl-params">
                    {params.map((p, i) => (
                      <label key={i}>Variável {`{{${i + 1}}}`}
                        <input value={p} onChange={e => setParams(prev => prev.map((x, j) => j === i ? e.target.value : x))} placeholder={i === 0 ? 'Ex.: nome do cliente' : 'Texto'} />
                      </label>
                    ))}
                  </div>
                )}
                <div className="mc-tpl-prev">
                  {sel.header?.format === 'TEXT' && sel.header.text && <b className="mc-tpl-prev-h">{sel.header.text}</b>}
                  <div>{_tplFill(sel.body, params)}</div>
                  {sel.footer && <div className="mc-tpl-prev-f">{sel.footer}</div>}
                  {(sel.buttons || []).map((b, i) => <div key={i} className="mc-tpl-prev-b">{b.text}</div>)}
                </div>
                {sel.header && sel.header.format && sel.header.format !== 'TEXT' && (
                  <div className="mc-tpl-warn">Este modelo tem cabeçalho com mídia — ainda não dá para enviar pelo Horos. Escolha outro.</div>
                )}
                {sel.category === 'MARKETING' && <div className="mc-tpl-note">Modelos de marketing são cobrados pelo Meta na sua conta do WhatsApp Business.</div>}
              </>
            )}
          </div>
        </div>
        {err && <div className="mc-tpl-err">{err}</div>}
        <div className="mc-tpl-f">
          <button className="mc-tpl-btn" onClick={onClose}>Cancelar</button>
          <button className="mc-tpl-btn mc-tpl-btn--p" onClick={send}
            disabled={!sel || missing || sending || (sel.header && sel.header.format && sel.header.format !== 'TEXT')}>
            {sending ? 'Enviando…' : 'Enviar modelo'}
          </button>
        </div>
      </div>
    </div>,
    document.body
  );
}

function MessagesPage() {
  // Theme
  const [theme, setTheme] = React.useState(() => {
    try { return localStorage.getItem('horos-theme') || 'dark'; } catch { return 'dark'; }
  });
  React.useEffect(() => {
    document.documentElement.dataset.theme = theme;
    try { localStorage.setItem('horos-theme', theme); } catch {}
  }, [theme]);

  // WA status
  const [waStatus, setWaStatus]   = React.useState('loading'); // loading | idle | connecting | qr | connected
  React.useEffect(() => { if (waStatus === 'connected') window.dispatchEvent(new CustomEvent('horos:wa-connected')); }, [waStatus]);   // Primeiros passos marca na hora
  const [qrData, setQrData]       = React.useState(null);
  const [waPhone, setWaPhone]     = React.useState(null);
  const [waProvider, setWaProvider] = React.useState(null); // 'evolution' (QR code) | 'cloud' (API oficial)
  const [tplModal, setTplModal]     = React.useState(false);
  const [waLabels, setWaLabels]   = React.useState([]);
  const [waBanInfo, setWaBanInfo] = React.useState(null); // notificação wa_banned mais recente
  const [userDidDisconnect, setUserDidDisconnect] = React.useState(() => localStorage.getItem('horos-wa-user-disconnected') === '1');

  // Data
  const [convs, setConvs]           = React.useState([]);
  const [readConvIds, setReadConvIds] = React.useState(() => {
    try {
      const stored = localStorage.getItem('horos-read-convs');
      return stored ? new Set(JSON.parse(stored)) : new Set();
    } catch { return new Set(); }
  });
  const [convsLoading, setConvsLoading] = React.useState(true);
  const [leads, setLeads]           = React.useState([]);
  const [selectedId, setSelectedId] = React.useState(null);
  const [msgs, setMsgs]             = React.useState([]);
  const [lead, setLead]             = React.useState(null);
  const [loadingMsgs, setLoadingMsgs] = React.useState(false);
  const [hasMore, setHasMore]         = React.useState(false);
  const [loadingMore, setLoadingMore] = React.useState(false);

  // Intelligence
  const [suggestion, setSuggestion] = React.useState(null);
  // Botão "Ass": liga/desliga o copiloto de verdade (servidor para de gerar = sem gasto de tokens)
  const [copilotOn, setCopilotOn] = React.useState(false);
  React.useEffect(() => {
    const tok = window.HOROS_SESSION?.access_token;
    if (!tok) return;
    fetch('/api/intelligence/copilot', { headers: { Authorization: `Bearer ${tok}` } })
      .then(r => r.ok ? r.json() : null)
      .then(d => { if (d) { setCopilotOn(!!d.enabled); setCopilotKnown(d.allowed !== false && !d.turnedOff); } })
      .catch(() => {});
  }, []);
  // Janela "Respostas prontas no WhatsApp" (antes no onboarding): só aqui em Mensagens, uma vez por pessoa,
  // com o WhatsApp conectado e o copiloto ainda desligado.
  const [copilotKnown, setCopilotKnown] = React.useState(false);
  const [copilotIntro, setCopilotIntro] = React.useState(false);
  const copilotIntroKey = 'horos-copilot-intro:' + (window.HOROS_USER?.id || '');
  React.useEffect(() => {
    if (!copilotKnown || copilotOn || waStatus !== 'connected') return;
    try { if (localStorage.getItem(copilotIntroKey)) return; } catch { return; }
    const t = setTimeout(() => setCopilotIntro(true), 900);
    return () => clearTimeout(t);
  }, [copilotKnown, copilotOn, waStatus]);
  function closeCopilotIntro(activate) {
    try { localStorage.setItem(copilotIntroKey, '1'); } catch {}
    setCopilotIntro(false);
    if (activate) toggleCopilot(true);
  }
  // Recuperação do que o Realtime não entregou (mensagens, etiquetas, não lidas, nomes, conversas novas):
  // busca só conversas com updated_at > última conferência (coluna mantida por trigger no banco) e
  // aplica com applyConvRow — que também traz as mensagens novas da conversa aberta.
  // A cada 5s se o canal Realtime não está ativo, 20s se está; e ao voltar para a aba / reconectar.
  const syncSinceRef = React.useRef(new Date(Date.now() - 120000).toISOString());
  React.useEffect(() => {
    if (waStatus !== 'connected') return;
    let stopped = false, running = false, timer = null;
    async function syncChanges() {
      const uid = window.horosActUid?.() || window.HOROS_USER?.id;
      if (stopped || running || document.hidden || !uid) return;
      running = true;
      try {
        const { data, error } = await window.HOROS_SB.from('conversations')
          .select('*')
          .eq('user_id', uid)
          .gt('updated_at', syncSinceRef.current)
          .order('updated_at', { ascending: false })
          .limit(300);
        if (stopped || error || !data?.length) return;
        syncSinceRef.current = data[0].updated_at;
        for (const row of data) applyConvRow(row, { trustUnread: true });
      } finally { running = false; }
    }
    syncChangesRef.current = syncChanges;
    const tick = () => {
      if (stopped) return;
      syncChanges();
      timer = setTimeout(tick, rtHealthyRef.current ? 20000 : 5000);
    };
    timer = setTimeout(tick, 3000);
    const onBack = () => { if (!document.hidden) syncChanges(); };
    document.addEventListener('visibilitychange', onBack);
    window.addEventListener('focus', onBack);
    window.addEventListener('online', onBack);
    return () => {
      stopped = true;
      clearTimeout(timer);
      if (syncChangesRef.current === syncChanges) syncChangesRef.current = null;
      document.removeEventListener('visibilitychange', onBack);
      window.removeEventListener('focus', onBack);
      window.removeEventListener('online', onBack);
    };
  }, [waStatus]);

  async function toggleCopilot(next) {
    setCopilotOn(next);                       // otimista
    if (!next) setSuggestion(null);
    const tok = window.HOROS_SESSION?.access_token;
    try {
      const r = await fetch('/api/intelligence/copilot', {
        method: 'POST',
        headers: { Authorization: `Bearer ${tok}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ enabled: next }),
      });
      if (!r.ok) {
        setCopilotOn(!next);                  // desfaz se não salvou
        // Pro: acabou o teste de 50 sugestões → aviso de upgrade
        if (r.status === 402) {
          const up = window.horosUpgradeInfo ? window.horosUpgradeInfo() : { text: 'O copiloto sem limite faz parte do plano Max.' };
          window.dispatchEvent(new CustomEvent('horos:toast', { detail: { module: 'system', title: 'O teste do copiloto acabou', body: 'O copiloto sem limite faz parte do plano Max. ' + up.text } }));
        }
      } else if (next && window.horosLoadPlan) window.horosLoadPlan(true);
    } catch { setCopilotOn(!next); }
  }

  // UI
  const [filter, setFilter]         = React.useState('all');
  const [search, setSearch]         = React.useState('');
  const [showNewConv, setShowNewConv] = React.useState(false);
  const [newConvPhone, setNewConvPhone] = React.useState('');
  const [drawer, setDrawer]         = React.useState(null);  // leadId
  const [labelVisible, setLabelVisible] = React.useState({});
  const [replyTo, setReplyTo]       = React.useState(null);
  const [isSyncing, setIsSyncing]       = React.useState(false);
  const [isSyncingConv, setIsSyncingConv] = React.useState(false);
  // Painel direito (dados do lead/contato): fechado por padrão e ao abrir outra conversa;
  // só abre pelo botão do cabeçalho ou pela aba lateral
  const [panelOpen, setPanelOpen]   = React.useState(false);
  const [dragOver, setDragOver]     = React.useState(false);
  const dragCounter                 = React.useRef(0);
  const [quickReplyText, setQuickReplyText] = React.useState('');
  const [forwardMsg, setForwardMsg] = React.useState(null); // msg to forward
  const [actionModal, setActionModal] = React.useState(null); // { type } | null

  const selectedIdRef      = React.useRef(null);
  const pollRef            = React.useRef(null);
  const qrRefreshRef       = React.useRef(null);
  const bottomRef          = React.useRef(null);
  const userInitiatedRef   = React.useRef(false);
  const explicitDisconnRef = React.useRef(false); // true após user clicar Desconectar, false ao conectar
  const reconnGraceRef          = React.useRef(false);
  const missSinceRef            = React.useRef(0);
  const wasConnectedRef         = React.useRef(false);
  const mainPollRef             = React.useRef(null);
  const realtimeDisconnTimerRef = React.useRef(null); // debounce de desconexão via Realtime
  const loadingMoreRef          = React.useRef(false); // guard para evitar double-trigger ao rolar
  const msgsCacheRef       = React.useRef(new Map()); // id → msgs[] — exibição instantânea ao re-clicar
  const histPageRef = React.useRef(new Map()); // convId → próxima página do histórico do WhatsApp (0 = acabou)
  const firstConvsLoadDone = React.useRef(false);
  const chatInputRef       = React.useRef(null);
  const prevWaStatusRef    = React.useRef('loading'); // detecta transição → connected para retry de sync
  const sendOptSeqRef      = React.useRef(0);         // contador monotônico para IDs otimistas únicos
  const lastViewedRef      = React.useRef({});        // convId → timestamp (ms) de quando o usuário saiu

  React.useEffect(() => { selectedIdRef.current = selectedId; }, [selectedId]);

  // Sinaliza ao notification-service qual conversa está aberta (suprime som/toast/badge)
  React.useEffect(() => { window._horosOpenConvId = selectedId || null; }, [selectedId]);

  // Informa notification-service que o usuário está na tela de Mensagens
  React.useEffect(() => {
    window._horosOnMessagesPage = true;
    window.HorosNotif?.resetWaUnread?.(); // badge Mensagens na sidebar → 0 ao entrar
    return () => { window._horosOnMessagesPage = false; window._horosOpenConvId = null; };
  }, []);

  // Foca o input ao ativar reply
  React.useEffect(() => { if (replyTo) chatInputRef.current?.focus(); }, [replyTo]);

  // Campo de mensagem sempre pronto para digitar (como no WhatsApp Web):
  // volta o foco ao abrir conversa, ao reconectar, ao voltar para a aba, depois de
  // cliques no chat, e qualquer tecla digitada fora de outro campo vai para ele.
  const isEditable = (el) => !!el && (el.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(el.tagName));
  const focusComposer = React.useCallback(() => {
    if (!selectedIdRef.current || !chatInputRef.current) return;
    const ae = document.activeElement;
    if (isEditable(ae) && !ae.classList.contains('mc-compose-ta')) return; // usuário está em outro campo
    if (window.getSelection && String(window.getSelection()).length) return; // copiando texto
    chatInputRef.current.focus();
  }, []);

  React.useEffect(() => { if (selectedId && waStatus === 'connected') setTimeout(focusComposer, 60); }, [selectedId, waStatus, focusComposer]);

  // Abrir uma conversa específica vindo de outra tela (Horos 360 → "Abrir em Mensagens")
  // ou da notificação no celular (messages.html?conv=<id>)
  React.useEffect(() => {
    if (!convs.length) return;
    let pending = null;
    const qConv = new URLSearchParams(location.search).get('conv');
    if (qConv) { pending = qConv; history.replaceState(null, '', location.pathname); }
    else try { pending = sessionStorage.getItem('horos:open-conv'); } catch {}
    if (!pending) return;
    try { sessionStorage.removeItem('horos:open-conv'); } catch {}
    if (convs.some(c => c.id === pending)) { selectConv(pending); return; }
    // fora das 200 mais recentes: busca a conversa e coloca no topo da lista
    window.HOROS_SB.from('conversations').select('*').eq('id', pending).maybeSingle().then(({ data }) => {
      if (!data) return;
      setConvs(prev => prev.some(c => c.id === data.id) ? prev : [_mapConv(data), ...prev]);
      setTimeout(() => selectConv(data.id), 50);
    });
  }, [convs.length]);

  React.useEffect(() => {
    const onKey = (e) => {
      if (e.defaultPrevented || e.ctrlKey && e.key.toLowerCase() !== 'v' || e.metaKey || e.altKey) return;
      if (isEditable(document.activeElement)) return;
      if (document.querySelector('[role="dialog"], .mc-modal-overlay, .mc-client-drawer-frame')) return;
      const printable = e.key.length === 1 || (e.ctrlKey && e.key.toLowerCase() === 'v');
      if (printable) focusComposer();
    };
    const onUp = (e) => {
      const t = e.target;
      if (!t.closest || !t.closest('.msg-content')) return;
      if (t.closest('button, a, input, textarea, select, audio, video, [contenteditable="true"], .mc-emoji-picker, .mc-qr-pop')) return;
      setTimeout(focusComposer, 0);
    };
    const onVisible = () => { if (document.visibilityState === 'visible') setTimeout(focusComposer, 50); };
    document.addEventListener('keydown', onKey, true);
    document.addEventListener('mouseup', onUp);
    document.addEventListener('visibilitychange', onVisible);
    window.addEventListener('focus', onVisible);
    return () => {
      document.removeEventListener('keydown', onKey, true);
      document.removeEventListener('mouseup', onUp);
      document.removeEventListener('visibilitychange', onVisible);
      window.removeEventListener('focus', onVisible);
    };
  }, [focusComposer]);

  // ── Polling de status WA ─────────────────────────────────────────────────
  React.useEffect(() => {
    let cancelled = false;

    async function waitForSession(ms = 8000) {
      const t0 = Date.now();
      while (Date.now() - t0 < ms) {
        if (window.HOROS_SESSION?.access_token) return true;
        await new Promise(r => setTimeout(r, 120));
      }
      return false;
    }

    // Quedas curtas (428, 515, 408…) a Evolution reconecta sozinha em segundos. Se o WhatsApp já
    // estava conectado nesta tela, só mostra "desconectado" após 45s seguidos sem conexão — antes
    // bastavam 2 checagens e o corretor via a tela cair e clicava em Reconectar à toa.
    function miss() {
      if (cancelled) return;
      const first = !reconnGraceRef.current;
      if (first) { reconnGraceRef.current = true; missSinceRef.current = Date.now(); }
      const limit = wasConnectedRef.current ? 45000 : 0;
      if (!first && Date.now() - missSinceRef.current >= limit) {
        wasConnectedRef.current = false;
        setWaStatus('idle');
        clearInterval(pollRef.current);
        pollRef.current = setInterval(poll, 3000);
      } else if (first && wasConnectedRef.current) {
        clearInterval(pollRef.current);
        pollRef.current = setInterval(poll, 5000);
      }
    }

    async function poll() {
      const tok = window.HOROS_SESSION?.access_token;
      if (!tok || cancelled) return;
      const ctrl = new AbortController();
      const timer = setTimeout(() => ctrl.abort(), 10000);
      try {
        const r = await fetch('/api/whatsapp-instance', {
          headers: { 'Authorization': `Bearer ${tok}` },
          signal: ctrl.signal,
        });
        if (!r.ok) { miss(); return; }
        // r.json() ainda coberto pelo signal — clearTimeout só no finally
        const d = await r.json();
        if (cancelled || explicitDisconnRef.current) return;
        if (d.waLabels?.length) setWaLabels(_sortWaLabels(d.waLabels));
        if (d.status === 'connected') {
          reconnGraceRef.current = false;
          wasConnectedRef.current = true;
          setWaStatus('connected');
          if (d.phone) setWaPhone(d.phone);
          setWaProvider(d.provider || 'evolution');
          // Reduz poll para 30s em vez de parar — detecta desconexão futura do celular
          clearInterval(pollRef.current);
          pollRef.current = setInterval(poll, 30000);
        } else if (d.status === 'connecting' && userInitiatedRef.current) {
          reconnGraceRef.current = false;
          setWaStatus(d.qr ? 'qr' : 'connecting');
          if (d.qr) setQrData(d.qr);
        } else {
          miss();
        }
      } catch (e) {
        miss();
      } finally {
        clearTimeout(timer);
      }
    }

    mainPollRef.current = poll;

    async function init() {
      const ok = await waitForSession();
      if (cancelled) return;
      if (!ok) { setWaStatus('idle'); return; }
      await poll();
      // poll() já pode ter ligado o intervalo de 30 s (conectado): não cria um segundo sem dono
      if (!cancelled && !pollRef.current) pollRef.current = setInterval(poll, 3000);
    }

    init();
    return () => { cancelled = true; clearInterval(pollRef.current); pollRef.current = null; clearInterval(qrRefreshRef.current); };
  }, []);

  // ── Carrega conversas e leads — sempre que há sessão, independente do WA ──
  React.useEffect(() => {
    const prevStatus = prevWaStatusRef.current;
    prevWaStatusRef.current = waStatus;
    // Transição de qualquer estado não-conectado para conectado (ex: após scan do QR).
    // Nesse momento o Baileys ainda pode estar processando o chats.set do WhatsApp —
    // disparamos o sync imediatamente E uma segunda tentativa 30s depois para capturar
    // os chats que chegam em lote logo após a conexão.
    // 'loading' → 'connected' = só abriu a tela com o WhatsApp já conectado (não é conexão nova).
    // Antes contava como conexão: toda abertura rodava a sincronização geral 2× (ignorando a regra
    // dos 10 min), refazia a busca de contatos e baixava a lista de novo.
    const justConnected = prevStatus !== 'connected' && prevStatus !== 'loading' && waStatus === 'connected';
    if (justConnected) {
      sessionStorage.removeItem('horos-contacts-synced');
      // Sync proativo 15s após conectar — Baileys precisa de tempo para sincronizar
      // antes de findContacts/findChats retornarem dados completos
      const _tok = window.HOROS_SESSION?.access_token;
      if (_tok) setTimeout(() => {
        fetch('/api/whatsapp-sync?contacts=1', { headers: { Authorization: `Bearer ${_tok}` } })
          .then(r => r.ok && r.json())
          .then(result => { if ((result?.updated || 0) > 0) loadConvs(); })
          .catch(() => {});
      }, 15000);
    }

    // A abertura da tela acabou de carregar tudo: não baixa de novo (só ao conectar agora)
    const listsFresh = !justConnected && Date.now() - listsLoadedAtRef.current < 20000;
    if (waStatus === 'connected') {
      (justConnected ? loadConvs() : loadConvsShared()).then(convData => {
        // Backfill de fotos: se há conversas sem foto, busca uma vez por sessão
        const hasMissingPic = Array.isArray(convData) && convData.some(c => !c.profile_pic_url);
        if (hasMissingPic && !sessionStorage.getItem('horos-pic-backfill')) {
          sessionStorage.setItem('horos-pic-backfill', '1');
          const tok = window.HOROS_SESSION?.access_token;
          fetch('/api/whatsapp-sync?pics=1', { headers: { 'Authorization': `Bearer ${tok}` } })
            .catch(() => {});
        }
        // Pre-fetch mensagens das primeiras conversas em background (clique fica instantâneo)
        setTimeout(() => prefetchTopConvs(convData), 800);
      });
      if (!listsFresh) { loadLeads(); loadWaLabels(); }

      // Recache áudios antigos sem storagePath — roda uma vez por sessão, fire-and-forget
      if (!sessionStorage.getItem('horos-audio-recache')) {
        sessionStorage.setItem('horos-audio-recache', '1');
        const tok = window.HOROS_SESSION?.access_token;
        if (tok) {
          (function recacheLoop() {
            fetch('/api/whatsapp-sync?recache_audio=1', {
              headers: { 'Authorization': `Bearer ${tok}` },
            }).then(r => r.ok && r.json()).then(res => {
              if (res?.remaining > 0) setTimeout(recacheLoop, 5000);
            }).catch(() => {});
          })();
        }
      }

      // Absorve conversas-fantasma @lid (etiquetas sem nome/foto) nas conversas reais — 1×/sessão
      if (!sessionStorage.getItem('horos-lid-repair')) {
        sessionStorage.setItem('horos-lid-repair', '1');
        const tok = window.HOROS_SESSION?.access_token;
        if (tok) fetch('/api/whatsapp-sync?lid_repair=1', { headers: { 'Authorization': `Bearer ${tok}` } })
          .then(r => r.ok && r.json())
          .then(res => { if ((res?.merged || 0) + (res?.pics || 0) > 0) loadConvs(); })
          .catch(() => {});
      }

      const lastSync = parseInt(localStorage.getItem('horos-wa-last-sync') || '0');
      const staleness = Date.now() - lastSync;

      if (justConnected) {
        // Conexão recém-estabelecida: sync agora + retry em 30s (captura histórico tardio do WA)
        syncAll();
        setTimeout(() => syncAll(), 30000);
      } else if (staleness > 10 * 60 * 1000) {
        // Reload de página com WA já conectado: re-sync se passou mais de 10 min
        // (era 90 s — cada abertura de Mensagens pesava no banco; o webhook já traz o que chega)
        syncAll();
      }
    } else if (waStatus === 'idle') {
      // WA desconectado mas sessão disponível → mostra histórico do banco
      loadConvsShared().then(convData => {
        const hasMissingPic = Array.isArray(convData) && convData.some(c => !c.profile_pic_url);
        if (hasMissingPic && !sessionStorage.getItem('horos-pic-backfill-idle')) {
          sessionStorage.setItem('horos-pic-backfill-idle', '1');
          const tok = window.HOROS_SESSION?.access_token;
          if (tok) fetch('/api/whatsapp-sync?pics=1', { headers: { 'Authorization': `Bearer ${tok}` } }).catch(() => {});
        }
      });
      if (!listsFresh) { loadLeads(); loadWaLabels(); }
    }
  }, [waStatus]);

  // Uma carga da lista por vez (2026-10-11): a abertura da tela, o "conectado" do WhatsApp e
  // a busca de nomes pediam a lista inteira (~5 mil conversas, 6 páginas) quase juntos — 3×
  // nos primeiros 12 s. Pedido no meio de uma carga = mais uma no fim, não outra em paralelo.
  const convsLoadRef = React.useRef({ p: null, again: false, at: 0 });
  const contactsSyncRef = React.useRef(false);
  const listsLoadedAtRef = React.useRef(0);
  function loadConvs() {
    const st = convsLoadRef.current;
    if (st.p) { st.again = true; return st.p; }
    const run = async () => {
      let data;
      do { st.again = false; data = await _loadConvsNow(); } while (st.again);
      st.at = Date.now(); st.data = data;
      return data;
    };
    st.p = run().finally(() => { st.p = null; });
    return st.p;
  }
  // Usa a carga em andamento ou a de menos de 20 s, sem pedir outra
  function loadConvsShared() {
    const st = convsLoadRef.current;
    if (st.p) return st.p;
    if (st.data && Date.now() - st.at < 20000) return Promise.resolve(st.data);
    return loadConvs();
  }

  async function _loadConvsNow() {
    const uid = window.horosActUid?.() || window.HOROS_USER?.id;
    if (!uid) return [];
    const PAGE = 1000;
    let all = [], from = 0, done = false;
    while (!done) {
      const { data, error } = await window.HOROS_SB
        .from('conversations')
        .select('*')
        .eq('user_id', uid)
        .order('last_message_at', { ascending: false, nullsFirst: false })
        .range(from, from + PAGE - 1);
      if (error) { console.error('[loadConvs]', error); break; }
      if (data?.length) all = all.concat(data);
      if (!data || data.length < PAGE) done = true;
      else from += PAGE;
    }
    if (all.length) {
      const dedupedAll = (() => {
        const byPhone = new Map();
        for (const row of all) {
          const p = row.contact_phone;
          if (!p) continue;
          const ex = byPhone.get(p);
          if (!ex) { byPhone.set(p, row); continue; }
          const keep = (row.last_message_at || '') > (ex.last_message_at || '') ? row : ex;
          const drop = keep === row ? ex : row;
          keep.labels = [...new Set([...(Array.isArray(keep.labels) ? keep.labels : []), ...(Array.isArray(drop.labels) ? drop.labels : [])])];
          byPhone.set(p, keep);
        }
        const keptIds = new Set([...byPhone.values()].map(r => r.id));
        return all.filter(row => !row.contact_phone || keptIds.has(row.id));
      })();
      const fresh = dedupedAll.map(_mapConv);
      setConvs(prev => {
        let stored = new Set();
        try { const s = localStorage.getItem('horos-read-convs'); if (s) stored = new Set(JSON.parse(s)); } catch {}
        if (!prev.length) {
          // Ao iniciar: todas as conversas começam com unread=0 — só incrementa em tempo real.
          return fresh.map(c => ({ ...c, unread: 0 }));
        }
        const prevById = new Map(prev.map(c => [c.id, c]));
        return fresh.map(c => {
          const ex = prevById.get(c.id);
          // Nunca aumenta unread via loadConvs — preserva estado local mais baixo
          if (ex && c.unread > ex.unread) c.unread = ex.unread;
          // Conversa selecionada ou já lida: mantém sempre 0
          if (c.id === selectedIdRef.current || stored.has(c.id)) c.unread = 0;
          return c;
        });
      });
    }
    if (!firstConvsLoadDone.current) {
      firstConvsLoadDone.current = true;
      setConvsLoading(false);
    }
    // Se há contatos sem nome ou sem foto (independente de etiquetas), enriquece via ?contacts=1 (uma vez por sessão)
    const isPhoneLike = (s) => s && /^\+?[\d\s\-()]{7,}$/.test(s.trim());
    const hasUnnamed   = all.some(c => !c.contact_name || isPhoneLike(c.contact_name));
    const hasMissingPic = all.some(c => !c.profile_pic_url && !(c.wa_jid || '').endsWith('@g.us'));
    if ((hasUnnamed || hasMissingPic) && !sessionStorage.getItem('horos-contacts-synced') && !contactsSyncRef.current) {
      const tok = window.HOROS_SESSION?.access_token;
      if (tok) {
        contactsSyncRef.current = true;   // uma sequência de tentativas por abertura, não uma por recarga
        // Retry até 5× com intervalos crescentes (20s, 40s, 60s, 80s, 100s).
        // stillNameless > 0 = Baileys ainda não resolveu todos os nomes (LID / agenda) —
        // continua tentando. Se updated > 0 recarga a lista imediatamente.
        const runContactsSync = (attempt) => {
          fetch('/api/whatsapp-sync?contacts=1', { headers: { Authorization: `Bearer ${tok}` } })
            .then(r => r.ok && r.json())
            .then(result => {
              if ((result?.updated || 0) > 0) loadConvs();
              if ((result?.stillNameless || 0) > 0 && attempt < 5) {
                setTimeout(() => runContactsSync(attempt + 1), (attempt + 1) * 20000);
              } else {
                sessionStorage.setItem('horos-contacts-synced', '1');
              }
            }).catch(() => {});
        };
        runContactsSync(0);
      }
    }
    return all;
  }

  // Pre-fetch silencioso das primeiras conversas — elimina delay no primeiro clique
  async function prefetchTopConvs(convList) {
    const uid = window.horosActUid?.() || window.HOROS_USER?.id;
    const top = (convList || []).slice(0, 8);
    for (const c of top) {
      if (msgsCacheRef.current.has(c.id) || !window.HOROS_SB) continue;
      let q = window.HOROS_SB.from('messages').select('*')
        .eq('conversation_id', c.id)
        .order('sent_at', { ascending: false, nullsFirst: false })
        .limit(200);
      if (uid) q = q.eq('user_id', uid);
      const { data } = await q;
      if (data?.length) msgsCacheRef.current.set(c.id, data.map(_mapMsg).reverse());
    }
  }

  async function loadLeads() {
    // Pagina de 1000 em 1000 (o PostgREST corta em 1000) — a aba Aguardando depende da lista completa
    const all = [];
    for (let from = 0; ; from += 1000) {
      const { data, error } = await window.HOROS_SB
        .from('leads')
        // só o que a lista usa (antes: a ficha inteira, `data`, de cada lead — 49 ms por página)
        .select('id, name, phone, temp, col, pipeline_id, assigned_to, score, profilePic:data->>profilePic')
        .eq('archived', false)
        .order('id')
        .range(from, from + 999);
      if (error) { if (!all.length) return; break; }
      all.push(...(data || []));
      if (!data || data.length < 1000) break;
    }
    for (const l of all) { l.data = l.profilePic ? { profilePic: l.profilePic } : {}; delete l.profilePic; }
    setLeads(all);
  }

  async function loadWaLabels() {
    const { data } = await window.HOROS_SB
      .from('wa_labels')
      .select('label_id, name, color')
      .eq('user_id', window.horosActUid?.() || window.HOROS_USER?.id);
    if (data?.length) setWaLabels(_sortWaLabels(data));
  }

  // ── Carrega histórico imediatamente ao montar — antes de saber status WA ──
  React.useEffect(() => {
    let cancelled = false;
    async function earlyLoad() {
      // HOROS_SB é setado sincronamente pelo auth-guard, antes do React montar.
      // Não precisamos aguardar HOROS_SESSION — o cliente Supabase já tem a sessão internamente.
      const t0 = Date.now();
      while (Date.now() - t0 < 3000) {
        if (window.HOROS_SB) break;
        await new Promise(r => setTimeout(r, 50));
      }
      if (cancelled || !window.HOROS_SB) { setConvsLoading(false); return; }
      listsLoadedAtRef.current = Date.now();
      loadConvs();
      loadLeads();
      loadWaLabels();
    }
    earlyLoad();
    return () => { cancelled = true; };
  }, []);

  // ── Ban/restrição WA ─────────────────────────────────────────────────────
  async function checkBanStatus() {
    // Usuário desconectou intencionalmente pelo Horos — não mostrar banner de sessão encerrada
    if (localStorage.getItem('horos-wa-user-disconnected') === '1') return;
    const since = new Date(Date.now() - 48 * 60 * 60 * 1000).toISOString();
    const uid = window.HOROS_USER?.id;
    if (!uid) return;
    // user_id explícito: só a RLS fazia o banco percorrer as notificações de todos por data (98 ms)
    const { data } = await window.HOROS_SB
      .from('notifications')
      .select('id, type, title, body, payload, created_at')
      .eq('user_id', uid)
      .in('type', ['wa_banned', 'wa_ban_warning'])
      .gte('created_at', since)
      .order('created_at', { ascending: false })
      .limit(1);
    if (data?.length) setWaBanInfo(data[0]);
  }

  React.useEffect(() => {
    let cancelled = false;
    let ch = null;

    async function init() {
      const t0 = Date.now();
      while (Date.now() - t0 < 8000) {
        if (window.HOROS_SESSION?.access_token) break;
        await new Promise(r => setTimeout(r, 120));
      }
      if (cancelled) return;
      await checkBanStatus();
      const uid = window.HOROS_USER?.id;
      if (!uid || cancelled) return;
      ch = window.HOROS_SB.channel('wa-status-rt')
        .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'notifications',
          filter: `user_id=eq.${uid}` }, (payload) => {
          if (payload.new?.type === 'wa_banned' || payload.new?.type === 'wa_ban_warning')
            setWaBanInfo(payload.new);
        })
        .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'wa_instances',
          filter: `user_id=eq.${window.horosActUid?.() || uid}` }, (payload) => {
          const newStatus = payload.new?.status;
          // Desconexão detectada via webhook → debounce 12s antes de mostrar idle
          // (absorve reconexões automáticas do WA sem piscar o banner de desconectado)
          if (newStatus === 'disconnected' && !explicitDisconnRef.current) {
            clearTimeout(realtimeDisconnTimerRef.current);
            reconnGraceRef.current = false;
            clearInterval(pollRef.current);
            if (mainPollRef.current) pollRef.current = setInterval(mainPollRef.current, 3000);
            realtimeDisconnTimerRef.current = setTimeout(() => {
              setWaStatus(prev => {
                if (prev === 'connected') return 'idle';
                return prev;
              });
            }, 12000);
          }
          // Reconexão detectada → cancela o timer de desconexão e confirma connected
          if (newStatus === 'connected') {
            clearTimeout(realtimeDisconnTimerRef.current);
            setWaStatus(prev => {
              if (prev !== 'connected') return 'connected';
              return prev;
            });
          }
        })
        .subscribe();
    }

    init();
    return () => { cancelled = true; if (ch) window.HOROS_SB.removeChannel(ch); };
  }, []);

  // Aplica uma linha de conversa vinda do Realtime OU da recuperação periódica (syncChanges)
  // opts.trustUnread (recuperação): o INSERT da mensagem pode ter sido perdido junto com o Realtime,
  // então vale o unread_count do banco (e a conversa sai da lista de "lidas").
  function applyConvRow(row, opts = {}) {
    const payload = { new: row };
    const updated = _mapConv(payload.new);
    if (opts.trustUnread && updated.unread > 0 && updated.id !== selectedIdRef.current) {
      _CLEARED_CONVS.delete(updated.id);
      try {
        const rs = new Set(JSON.parse(localStorage.getItem('horos-read-convs') || '[]'));
        if (rs.delete(updated.id)) localStorage.setItem('horos-read-convs', JSON.stringify([...rs]));
      } catch {}
      setReadConvIds(prev => { if (!prev.has(updated.id)) return prev; const n = new Set(prev); n.delete(updated.id); return n; });
    }
    setConvs(prev => {
      const existing = prev.find(c => c.id === updated.id);
      // Nunca aumenta unread via conversations UPDATE — incrementos vêm exclusivamente
      // do handler de messages INSERT (que checa fromMe). Aceita só valores menores
      // (ex: usuário leu no celular → DB zera → propagamos o 0). Exceção: recuperação.
      if (!opts.trustUnread && existing && updated.unread > existing.unread) updated.unread = existing.unread;
      // Conversa selecionada ou já lida: garante que nunca mostre não-lidas
      if (updated.id === selectedIdRef.current) updated.unread = 0;
      try {
        const s = localStorage.getItem('horos-read-convs');
        if (s && new Set(JSON.parse(s)).has(updated.id)) updated.unread = 0;
      } catch {}
      // Se nada relevante mudou (ex: clear_conversation_unread já foi aplicado por selectConv),
      // retorna prev sem criar novo array — evita re-render e reordenação visual desnecessária.
      if (existing &&
        existing.ts     === updated.ts   &&
        existing.last   === updated.last  &&
        existing.name   === updated.name  &&
        existing.unread === updated.unread &&
        existing.pic    === updated.pic   &&
        existing.pinned === updated.pinned &&
        existing.lastInboundAt === updated.lastInboundAt &&
        // etiquetas: sem isto, etiqueta colocada no WhatsApp só aparecia ao recarregar a página
        [...(existing.labels || [])].sort().join(',') === [...(updated.labels || [])].sort().join(',')
      ) return prev;
      const list = existing
        ? prev.map(c => c.id === updated.id ? updated : c)
        : [updated, ...prev];
      // Só re-ordena quando ts mudou (nova mensagem); mudanças de unread_count mantêm a posição.
      const tsChanged = !existing || existing.ts !== updated.ts;
      return tsChanged ? list.sort((a, b) => {
        if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
        return new Date(b.ts) - new Date(a.ts);
      }) : list;
    });
    // fallback: busca msgs recentes se a conversa selecionada foi atualizada
    // (cobre inserts server-side do webhook que podem não disparar INSERT event no Realtime)
    if (payload.new.id === selectedIdRef.current) {
      const _uid = window.horosActUid?.() || window.HOROS_USER?.id;
      let _q = window.HOROS_SB.from('messages').select('*')
        .eq('conversation_id', payload.new.id)
        .order('sent_at', { ascending: false }).limit(20);
      if (_uid) _q = _q.eq('user_id', _uid);
      _q
        .then(({ data }) => {
          if (!data) return;
          if (selectedIdRef.current !== payload.new.id) return;
          setMsgs(prev => {
            const merged = _mergeMsgs(prev, data.map(_mapMsg));
            msgsCacheRef.current.set(payload.new.id, merged);
            return merged;
          });
        });
    }

  }

  // ── Realtime ── sobe no mount; uid disponível via auth-guard antes do componente montar.
  // Monitora o status do canal: se cair (erro, timeout, fechado), reinscreve sozinho com espera
  // crescente e, ao voltar, roda syncChanges para buscar o que ficou perdido.
  const rtHealthyRef   = React.useRef(false);
  const syncChangesRef = React.useRef(null);
  React.useEffect(() => {
    const uid = window.horosActUid?.() || window.HOROS_USER?.id;
    if (!uid) return;
    let ch = null, dead = false, retry = 0, retryTimer = null;
    const connect = () => {
    if (dead) return;
    if (ch) { const old = ch; ch = null; window.HOROS_SB.removeChannel(old); }
    const thisCh = window.HOROS_SB.channel('msgs-rt-' + Date.now())
      .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages',
        filter: `user_id=eq.${uid}` }, (payload) => {
        const m = _mapMsg(payload.new);
        const cid = selectedIdRef.current;
        if (m.convId === cid) {
          setMsgs(prev => {
            const next = _mergeMsgs(prev, [m]);
            msgsCacheRef.current.set(cid, next);
            return next;
          });
        } else {
          // atualiza cache da conversa que não está aberta
          const cached = msgsCacheRef.current.get(m.convId);
          if (cached) msgsCacheRef.current.set(m.convId, _mergeMsgs(cached, [m]));
        }
        setConvs(prev => {
          const exists = prev.some(c => c.id === m.convId);
          if (!exists) {
            // conversa nova chegou via mensagem (INSERT em conversations ainda não propagado)
            // busca a conversa do banco e adiciona ao estado
            window.HOROS_SB.from('conversations').select('*').eq('id', m.convId).limit(1)
              .then(({ data }) => {
                if (data?.[0]) setConvs(p => {
                  if (p.some(c => c.id === data[0].id)) return p;
                  return [_mapConv(data[0]), ...p].sort((a, b) => new Date(b.ts) - new Date(a.ts));
                });
              });
            return prev;
          }
          // Mensagem nova incoming em conversa não selecionada:
          // Só conta como "nova" se o sentAt for POSTERIOR a quando o usuário saiu da conversa.
          // Isso evita que eventos Realtime atrasados (de msgs que já estavam visíveis) reativem o badge.
          const msgTimeMs = new Date(m.sentAt).getTime();
          const lastViewed = lastViewedRef.current[m.convId]; // sempre current (ref)
          const isGenuinelyNew = !m.fromMe && m.convId !== cid && (!lastViewed || msgTimeMs > lastViewed);
          if (isGenuinelyNew) {
            _CLEARED_CONVS.delete(m.convId); // síncrono — remove imediatamente do Set module-level
            setReadConvIds(prev => {
              const n = new Set(prev); n.delete(m.convId);
              try { localStorage.setItem('horos-read-convs', JSON.stringify([...n])); } catch {}
              return n;
            });
          }
          // Só atualiza ts/last se a mensagem for MAIS RECENTE que o timestamp atual da conversa.
          // Isso evita que eventos Realtime de mensagens antigas (ex: sync histórico) movam
          // a conversa para baixo na lista ou sobrescrevam o preview com texto antigo.
          const convRef = prev.find(c => c.id === m.convId);
          const isNewer = !convRef?.ts || m.sentAt > convRef.ts;
          const updated = prev.map(c => c.id === m.convId
            ? {
                ...c,
                ...(isNewer ? { last: m.body, ts: m.sentAt } : {}),
                unread: m.fromMe ? c.unread : (m.convId === cid ? 0 : (isGenuinelyNew && isNewer ? c.unread + 1 : c.unread)),
              }
            : c
          );
          // Só re-ordena se ts realmente mudou (conversa subiu na lista)
          return isNewer ? updated.sort((a, b) => new Date(b.ts) - new Date(a.ts)) : updated;
        });
      })
      .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'messages',
        filter: `user_id=eq.${uid}` }, (payload) => {
        // Cobre atualizações de status (delivered/read) e reações recebidas via webhook
        const m = _mapMsg(payload.new);
        const cid = selectedIdRef.current;
        if (m.convId === cid) {
          setMsgs(prev => prev.map(x => (x.id === m.id || (m.waId && x.waId === m.waId)) ? { ...x, status: m.status, reactions: m.reactions } : x));
        }
      })
      .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'conversations',
        filter: `user_id=eq.${uid}` }, (payload) => {
        const c = _mapConv(payload.new);
        setConvs(prev => {
          if (prev.some(x => x.id === c.id)) return prev;
          return [c, ...prev].sort((a, b) => new Date(b.ts) - new Date(a.ts));
        });
      })
      .on('postgres_changes', { event: 'UPDATE', schema: 'public', table: 'conversations',
        filter: `user_id=eq.${uid}` }, (payload) => applyConvRow(payload.new))
      .subscribe((status) => {
        if (dead || ch !== thisCh) return;          // eventos do canal antigo após reinscrição
        rtHealthyRef.current = status === 'SUBSCRIBED';
        if (status === 'SUBSCRIBED') {
          if (retry > 0) syncChangesRef.current?.(); // voltou depois de cair: recupera o perdido
          retry = 0;
        } else if (status === 'CHANNEL_ERROR' || status === 'TIMED_OUT' || status === 'CLOSED') {
          clearTimeout(retryTimer);
          retryTimer = setTimeout(connect, Math.min(30000, 2000 * Math.pow(2, retry++)));
        }
      });
    ch = thisCh;
    };
    connect();
    // Rede voltou → reinscreve já (o socket pode ter morrido em silêncio)
    const onOnline = () => { retry = Math.max(retry, 1); connect(); };
    window.addEventListener('online', onOnline);
    return () => {
      dead = true;
      clearTimeout(retryTimer);
      window.removeEventListener('online', onOnline);
      if (ch) window.HOROS_SB.removeChannel(ch);
    };
  }, []);

  // ── Realtime: ai_suggestions ─────────────────────────────────────────────
  React.useEffect(() => {
    const userId = window.horosActUid?.() || window.HOROS_USER?.id;
    if (!userId || !window.HOROS_SB) return;
    const ch = window.HOROS_SB.channel('ai-sugg-rt')
      .on('postgres_changes', {
        event: '*', schema: 'public', table: 'ai_suggestions',
        filter: `user_id=eq.${userId}`,
      }, payload => {
        const s = payload.new;
        if (!s) return;
        if (s.conversation_id === selectedIdRef.current) {
          setSuggestion(s);
          // Sync intellStatus from the suggestion status
          setConvs(prev => prev.map(c =>
            c.id === s.conversation_id
              ? { ...c, intellStatus: s.status === 'PENDING' ? 'SUGGESTION_PENDING' : (c.intellStatus === 'SUGGESTION_PENDING' ? 'IDLE' : c.intellStatus) }
              : c
          ));
        }
      })
      .subscribe();
    return () => window.HOROS_SB.removeChannel(ch);
  }, []);

  // ── Selecionar conversa ───────────────────────────────────────────────────
  async function selectConv(id) {
    // Registra quando o usuário saiu da conversa anterior
    if (selectedIdRef.current && selectedIdRef.current !== id) {
      lastViewedRef.current[selectedIdRef.current] = Date.now();
    }
    // Marca a conversa como lida IMEDIATAMENTE (síncrono, antes de qualquer setState)
    // _CLEARED_CONVS é um Set module-level — não depende de batching do React
    if (id) _CLEARED_CONVS.add(id);
    if (id !== selectedIdRef.current) setPanelOpen(false);
    setSelectedId(id);
    selectedIdRef.current = id; // sync com o ref imediatamente — useEffect é assíncrono
    setReplyTo(null);
    setSuggestion(null);
    setHasMore(false);
    setLoadingMore(false);
    if (!id) { setLead(null); return; }

    // Lookup instantâneo em memória — elimina flash do botão "Adicionar como Lead"
    const _snapConv = convs.find(c => c.id === id) || null;
    const _snapPhone = (_snapConv?.phone || _snapConv?.jid?.split('@')[0] || '').replace(/\D/g, '').replace(/^55/, '');
    const _instantLead = _snapPhone
      ? (leads.find(l => (l.phone || '').replace(/\D/g, '').replace(/^55/, '') === _snapPhone) || null)
      : null;
    setLead(_instantLead);

    // Tudo encapsulado — garante setLoadingMsgs(false) independente de qualquer erro
    let conv = _snapConv;
    try {
      // Cache hit → exibe imediatamente, sem spinner de mensagens
      const cached = msgsCacheRef.current.get(id);
      if (cached?.length) {
        setMsgs(cached);
        setHasMore(cached.length >= 200 || histPageRef.current.get(id) !== 0);
      } else {
        setMsgs([]);
        setLoadingMsgs(true);
      }

      const tok = window.HOROS_SESSION?.access_token;

      // Sync da conversa com o WhatsApp: roda a CADA clique (inclusive na conversa já aberta),
      // em paralelo com a leitura do banco. seq identifica este clique.
      const syncSeq = ++convSyncSeqRef.current;
      if (waStatus === 'connected' && tok) runConvSync(id, syncSeq);
      else setIsSyncingConv(false);

      // ── Dispara tudo em paralelo imediatamente ────────────────────────────
      // 1. Query de mensagens (principal — não espera mais nada)
      const _msgsUid = window.horosActUid?.() || window.HOROS_USER?.id;
      let msgsPromise = window.HOROS_SB.from('messages').select('*')
        .eq('conversation_id', id)
        .order('sent_at', { ascending: false, nullsFirst: false })
        .limit(200);
      if (_msgsUid) msgsPromise = msgsPromise.eq('user_id', _msgsUid);

      // 2. Fire-and-forget: sugestão IA, unread, mark-read no WA
      if (tok) {
        fetch(`/api/intelligence/suggestions?conversation_id=${id}&status=PENDING&limit=1`, {
          headers: { Authorization: `Bearer ${tok}` },
        }).then(r => r.ok ? r.json() : null)
          .then(d => { if (d?.suggestions?.[0] && selectedIdRef.current === id) setSuggestion(d.suggestions[0]); })
          .catch(() => {});
      }
      // O construtor do Supabase só tem .then (sem .catch): .catch dava erro e o resto do clique não rodava
      window.HOROS_SB?.rpc('clear_conversation_unread', { p_id: id }).then(() => {}, () => {});
      setConvs(prev => prev.map(c => c.id === id ? { ...c, unread: 0 } : c));
      setReadConvIds(prev => {
        const n = new Set(prev); n.add(id);
        try { localStorage.setItem('horos-read-convs', JSON.stringify([...n])); } catch {}
        return n;
      });
      fetch('/api/whatsapp-instance', {
        method: 'PATCH',
        headers: { 'Authorization': `Bearer ${tok}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ conversationId: id }),
      }).catch(() => {});

      // 3. detectLead fire-and-forget — só como fallback se não encontrou em memória
      if (_instantLead) {
        // A lista em memória pode estar velha (etapa mudada no CRM/outra aba) — confere no banco
        window.HOROS_SB.from('leads').select('*').eq('id', _instantLead.id).maybeSingle()
          .then(async ({ data: row }) => {
            const fresh = await _withOwnStage(row);
            if (!fresh || selectedIdRef.current !== id) return;
            if (fresh.archived) { setLead(null); return; }
            setLead(l => l?.id === fresh.id ? { ...l, ...fresh } : l);
            setLeads(prev => prev.map(l => l.id === fresh.id
              ? { ...l, name: fresh.name, phone: fresh.phone, temp: fresh.temp, col: fresh.col, score: fresh.score } : l));
          }, () => {});
      } else {
        if (!conv) {
          window.HOROS_SB.from('conversations').select('*').eq('id', id).single()
            .then(r => r.data ? _mapConv(r.data) : null)
            .then(c => { if (c && selectedIdRef.current === id) detectLead(c); })
            .catch(() => {});
        } else {
          detectLead(conv);
        }
      }

      // ── Aguarda só as mensagens ───────────────────────────────────────────
      if (selectedIdRef.current !== id) return;
      const { data, error } = await msgsPromise;
      if (error) console.error('[selectConv] msgs:', error);

      // Se o sync deste clique já terminou e aplicou dados mais novos, não sobrescreve
      if (data?.length && selectedIdRef.current === id && convSyncAppliedRef.current !== syncSeq) {
        const initial = data.map(_mapMsg).reverse();
        msgsCacheRef.current.set(id, initial);
        setMsgs(initial);
        setHasMore(data.length >= 200 || histPageRef.current.get(id) !== 0);
      }

    } catch (e) {
      console.error('[selectConv] CATCH', e);
    } finally {
      setLoadingMsgs(false);
      setTimeout(() => chatInputRef.current?.focus(), 80);
    }
  }

  // ── Sync de uma conversa com o WhatsApp ──────────────────────────────────
  // Usado no clique (selectConv), no botão da topbar e ao reconectar o WA.
  // seq: só o sync mais recente controla o spinner e evita sobrescrita por respostas antigas.
  const convSyncSeqRef     = React.useRef(0);
  const convSyncAppliedRef = React.useRef(0);
  // manual: clique no botão de sincronizar → aviso do resultado (antes não dizia nada e parecia
  // que o botão não funcionava) e mais tempo de espera.
  async function runConvSync(id, seq, manual = false) {
    const say = (title, body) => manual && window.dispatchEvent(new CustomEvent('horos:toast', {
      detail: { module: 'system', title, body } }));
    // Token renovado na hora: com a página aberta há mais de 1h o token antigo dava 401 e o
    // sync falhava calado
    const tok = window.horosToken ? await window.horosToken().catch(() => null) : window.HOROS_SESSION?.access_token;
    if (!id) return;
    if (!tok) { say('Não foi possível sincronizar', 'Sessão expirada. Recarregue a página.'); return; }
    setIsSyncingConv(true);
    const ctrl  = new AbortController();
    const timer = setTimeout(() => ctrl.abort(), manual ? 45000 : 15000);
    try {
      const r = await fetch(`/api/whatsapp-sync?convId=${encodeURIComponent(id)}`, {
        headers: { 'Authorization': `Bearer ${tok}` },
        cache: 'no-store',
        signal: ctrl.signal,
      });
      if (!r.ok) {
        const d = await r.json().catch(() => ({}));
        say('Não foi possível sincronizar', d.error || `O servidor respondeu ${r.status}. Tente de novo.`);
        return;
      }
      const syncResult = await r.json().catch(() => ({}));
      if (syncResult.contactName) {
        setConvs(prev => prev.map(c => c.id === id ? { ...c, name: syncResult.contactName } : c));
      }
      // Nenhuma mensagem nova veio do WhatsApp → o que está na tela já é o atual
      if (!syncResult.messages) { say('Conversa sincronizada', 'Nenhuma mensagem nova no WhatsApp.'); return; }
      const uid = window.horosActUid?.() || window.HOROS_USER?.id;
      let q = window.HOROS_SB.from('messages').select('*')
        .eq('conversation_id', id)
        .order('sent_at', { ascending: false, nullsFirst: false })
        .limit(200);
      if (uid) q = q.eq('user_id', uid);
      const { data } = await q;
      if (!data?.length) { say('Conversa sincronizada', 'Nenhuma mensagem nova no WhatsApp.'); return; }
      const synced = data.map(_mapMsg).reverse();
      const prevIds = new Set((msgsCacheRef.current.get(id) || []).map(m => m.id));
      const added = synced.filter(m => !prevIds.has(m.id)).length;
      msgsCacheRef.current.set(id, synced);
      if (selectedIdRef.current !== id || convSyncSeqRef.current !== seq) return;
      convSyncAppliedRef.current = seq;
      setMsgs(prev => {
        const merged = _mergeMsgs(prev, synced);
        msgsCacheRef.current.set(id, merged);
        return merged;
      });
      setHasMore(data.length >= 200 || histPageRef.current.get(id) !== 0);
      say('Conversa sincronizada', added
        ? `${added} ${added === 1 ? 'mensagem nova' : 'mensagens novas'} do WhatsApp.`
        : 'Tudo em dia com o WhatsApp.');
    } catch (e) {
      say('Não foi possível sincronizar', e?.name === 'AbortError'
        ? 'O WhatsApp demorou para responder. Tente de novo em instantes.'
        : 'Falha de conexão. Verifique a internet e tente de novo.');
    } finally {
      clearTimeout(timer);
      if (convSyncSeqRef.current === seq) setIsSyncingConv(false);
    }
  }

  // Botão "Sincronizar histórico" na topbar da conversa
  // Sempre roda: antes ficava travado enquanto o sync automático do clique na conversa
  // (até 15s) não terminava, e o botão parecia não fazer nada
  function syncConv() {
    if (!selectedId) return;
    runConvSync(selectedId, ++convSyncSeqRef.current, true);
  }

  // WA acabou de conectar com uma conversa aberta → sincroniza ela
  React.useEffect(() => {
    if (waStatus === 'connected' && selectedIdRef.current) {
      runConvSync(selectedIdRef.current, ++convSyncSeqRef.current);
    }
  }, [waStatus]);

  async function detectLead(conv) {
    const raw = (conv.phone || conv.jid?.split('@')[0] || '').replace(/\D/g, '');
    const digits = raw.replace(/^55/, '');
    if (!digits) return;
    const mask  = _maskPhone(digits);
    const full55 = '55' + digits;
    // Usa .in() em vez de .or() para lidar com caracteres especiais no formato mascarado.
    // Inclui full55 para leads criados via insert_wa_lead (telefone salvo com prefixo 55).
    const { data } = await window.HOROS_SB.from('leads').select('*')
      .eq('archived', false).in('phone', [digits, mask, full55]).limit(1);
    const found = await _withOwnStage(data?.[0] || null);
    if (selectedIdRef.current !== conv.id) return;
    setLead(found);
    if (found && conv.pic && !found.data?.profilePic) {
      window.HOROS_SB.rpc('upsert_lead_pic', { p_id: found.id, p_pic: conv.pic }).then(() => {}, () => {});
    }
  }

  async function loadMoreMsgs() {
    if (loadingMoreRef.current || loadingMore || !selectedId || msgs.length === 0) return;
    const oldest = msgs[0]?.sentAt;
    if (!oldest) return;
    loadingMoreRef.current = true;
    setLoadingMore(true);
    const tok = window.HOROS_SESSION?.access_token;
    try {
      // 1. Tenta carregar do Supabase primeiro (mais rápido)
      const { data: older } = await window.HOROS_SB.from('messages').select('*')
        .eq('conversation_id', selectedId)
        .lt('sent_at', oldest)
        .order('sent_at', { ascending: false, nullsFirst: false })
        .limit(200);
      if (older?.length) {
        const mapped = older.map(_mapMsg).reverse();
        setMsgs(prev => {
          const existingIds = new Set(prev.map(m => m.id));
          const newOlder = mapped.filter(m => !existingIds.has(m.id));
          return newOlder.length ? [...newOlder, ...prev] : prev;
        });
        // ainda pode haver mais no WhatsApp mesmo com menos de 200 no banco
        setHasMore(older.length >= 200 || histPageRef.current.get(selectedId) !== 0);
      } else {
        // 2. Não tem no Supabase → importa o histórico do WhatsApp página por página
        //    (?history=1 — a Evolution ignora filtro por data, mas pagina certo) até
        //    aparecerem mensagens mais antigas que a primeira da tela
        const stored = histPageRef.current.get(selectedId);
        let page = stored === undefined ? 1 : stored;
        let found = null;
        for (let i = 0; i < 15 && page; i++) {
          const tk = window.horosToken ? await window.horosToken() : tok;
          const r = await fetch(`/api/whatsapp-sync?convId=${encodeURIComponent(selectedId)}&history=1&page=${page}`,
            { headers: { 'Authorization': `Bearer ${tk}` } });
          const j = r.ok ? await r.json().catch(() => null) : null;
          if (!j?.history) { page = 0; break; }
          page = j.nextPage || 0;
          histPageRef.current.set(selectedId, page);
          if (j.inserted) {
            const { data: synced } = await window.HOROS_SB.from('messages').select('*')
              .eq('conversation_id', selectedId)
              .lt('sent_at', oldest)
              .order('sent_at', { ascending: false, nullsFirst: false })
              .limit(200);
            if (synced?.length) { found = synced; break; }
          }
        }
        if (found) {
          const mapped = found.map(_mapMsg).reverse();
          setMsgs(prev => {
            const existingIds = new Set(prev.map(m => m.id));
            const newOlder = mapped.filter(m => !existingIds.has(m.id));
            return newOlder.length ? [...newOlder, ...prev] : prev;
          });
          setHasMore(true);
        } else { setHasMore(false); }
      }
    } catch { setHasMore(false); }
    finally { loadingMoreRef.current = false; setLoadingMore(false); }
  }

  // ── Conectar / desconectar ────────────────────────────────────────────────
  async function connectWa() {
    userInitiatedRef.current = true;
    explicitDisconnRef.current = false;
    reconnGraceRef.current = false;
    localStorage.removeItem('horos-wa-user-disconnected');
    setWaStatus('connecting');
    const tok = window.HOROS_SESSION?.access_token;

    // Cria a instância e tenta obter QR
    try {
      const r = await fetch('/api/whatsapp-instance', {
        method: 'POST', headers: { 'Authorization': `Bearer ${tok}` },
      });
      if (r.ok) {
        const d = await r.json();
        if (d.status === 'connected') {
          // Instância ainda ativa — vai direto para connected
          setWaStatus('connected');
          if (d.phone) setWaPhone(d.phone);
          setWaProvider(d.provider || 'evolution');
          return;
        } else if (d.qr) {
          setQrData(d.qr);
          setWaStatus('qr');
        } else {
          // QR ainda não gerado — tenta a cada 2s por até 20s
          let attempts = 0;
          const earlyQr = setInterval(async () => {
            attempts++;
            if (attempts > 10) { clearInterval(earlyQr); return; }
            try {
              const rq = await fetch('/api/whatsapp-instance?qr=1', { headers: { 'Authorization': `Bearer ${tok}` } });
              if (!rq.ok) return;
              const dq = await rq.json();
              if (dq.qr) { setQrData(dq.qr); setWaStatus('qr'); clearInterval(earlyQr); }
            } catch {}
          }, 2000);
        }
      }
    } catch {}

    clearInterval(pollRef.current);
    clearInterval(qrRefreshRef.current);
    let connectFailCount = 0;
    pollRef.current = setInterval(async () => {
      try {
        const r2 = await fetch('/api/whatsapp-instance', { headers: { 'Authorization': `Bearer ${tok}` } });
        if (!r2.ok) return;
        const d2 = await r2.json();
        if (d2.waLabels?.length) setWaLabels(_sortWaLabels(d2.waLabels));
        if (d2.status === 'connected') {
          connectFailCount = 0;
          setWaStatus('connected');
          if (d2.phone) setWaPhone(d2.phone);
          setWaProvider(d2.provider || 'evolution');
          clearInterval(qrRefreshRef.current);
          // Passa para o poll lento do init (30s) em vez de parar
          clearInterval(pollRef.current);
          if (mainPollRef.current) pollRef.current = setInterval(mainPollRef.current, 30000);
        } else if (d2.status === 'connecting') {
          connectFailCount = 0;
          setWaStatus(prev => prev === 'qr' ? 'qr' : 'connecting');
        } else {
          // Grace: ignora 1 resposta disconnected durante QR/connecting para evitar falso-negativo
          connectFailCount++;
          if (connectFailCount >= 2) {
            setWaStatus('idle');
            clearInterval(pollRef.current);
            clearInterval(qrRefreshRef.current);
          }
        }
      } catch {}
    }, 3000);

    // Refresh do QR a cada 15s — QR expira ~20s, 15s garante código sempre válido
    qrRefreshRef.current = setInterval(async () => {
      try {
        const r3 = await fetch('/api/whatsapp-instance?qr=1', { headers: { 'Authorization': `Bearer ${tok}` } });
        if (!r3.ok) return;
        const d3 = await r3.json();
        if (d3.qr) setQrData(d3.qr);
      } catch {}
    }, 15000);
  }

  async function refreshQr() {
    const tok = window.HOROS_SESSION?.access_token;
    if (!tok) return;
    try {
      const r = await fetch('/api/whatsapp-instance?qr=1', { headers: { 'Authorization': `Bearer ${tok}` } });
      if (!r.ok) return;
      const d = await r.json();
      if (d.qr) setQrData(d.qr);
    } catch {}
  }

  async function disconnectWa() {
    userInitiatedRef.current = false;
    explicitDisconnRef.current = true;
    reconnGraceRef.current = false;
    localStorage.setItem('horos-wa-user-disconnected', '1');
    localStorage.removeItem('horos-wa-last-sync'); // força sync completo ao reconectar
    setUserDidDisconnect(true);
    setConvs([]);
    setSelectedId(null);
    setWaBanInfo(null);
    setWaStatus('idle');
    clearInterval(pollRef.current);
    clearInterval(qrRefreshRef.current);
    const tok = window.HOROS_SESSION?.access_token;
    fetch('/api/whatsapp-instance', {
      method: 'DELETE', headers: { 'Authorization': `Bearer ${tok}` },
    }).catch(() => {});
  }

  // ── Toggle etiqueta WA numa conversa ─────────────────────────────────────
  async function toggleLabel(convId, labelId, active) {
    const tok = window.HOROS_SESSION?.access_token;
    if (!tok || !convId || !labelId) return;
    const action = active ? 'remove' : 'add';
    // Optimistic update
    setConvs(prev => prev.map(c => {
      if (c.id !== convId) return c;
      const labels = active
        ? c.labels.filter(l => l !== labelId)
        : [...c.labels, labelId];
      return { ...c, labels };
    }));
    try {
      const r = await fetch('/api/whatsapp-instance', {
        method: 'PUT',
        headers: { 'Authorization': `Bearer ${tok}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ conversationId: convId, labelId, labelAction: action }),
      });
      if (r.ok) {
        const d = await r.json();
        if (Array.isArray(d.labels)) {
          setConvs(prev => prev.map(c => c.id === convId ? { ...c, labels: d.labels } : c));
        }
      } else {
        // Rollback + aviso: o WhatsApp não aceitou (o servidor não grava o que o WA recusou)
        setConvs(prev => prev.map(c => {
          if (c.id !== convId) return c;
          const labels = active ? [...c.labels, labelId] : c.labels.filter(l => l !== labelId);
          return { ...c, labels };
        }));
        const d = await r.json().catch(() => ({}));
        window.dispatchEvent(new CustomEvent('horos:toast', { detail: {
          module: 'system', title: active ? 'Etiqueta não removida' : 'Etiqueta não adicionada',
          body: d.error || 'O WhatsApp não aceitou a alteração. Tente novamente.',
        } }));
      }
    } catch {
      // Rollback
      setConvs(prev => prev.map(c => {
        if (c.id !== convId) return c;
        const labels = active ? [...c.labels, labelId] : c.labels.filter(l => l !== labelId);
        return { ...c, labels };
      }));
    }
  }

  // ── Etiqueta em várias conversas de uma vez (modo seleção da lista) ───────
  // Só mexe em quem precisa; 4 por vez; a tela só muda quando o WhatsApp confirma.
  async function bulkLabel(list, labelId, add, onProgress) {
    const id = String(labelId);
    const todo = list.filter(c => add ? !(c.labels || []).includes(id) : (c.labels || []).includes(id));
    let done = 0, ok = 0, fail = 0, i = 0;
    onProgress && onProgress(0, todo.length);
    const worker = async () => {
      while (i < todo.length) {
        const conv = todo[i++];
        try {
          const tok = window.horosToken ? await window.horosToken() : window.HOROS_SESSION?.access_token;
          const r = await fetch('/api/whatsapp-instance', {
            method: 'PUT',
            headers: { 'Authorization': `Bearer ${tok}`, 'Content-Type': 'application/json' },
            body: JSON.stringify({ conversationId: conv.id, labelId: id, labelAction: add ? 'add' : 'remove' }),
          });
          const d = await r.json().catch(() => ({}));
          if (!r.ok) throw new Error(d.error || 'recusado');
          const labels = Array.isArray(d.labels) ? d.labels.map(String)
            : add ? [...new Set([...(conv.labels || []), id])] : (conv.labels || []).filter(l => l !== id);
          setConvs(prev => prev.map(c => c.id === conv.id ? { ...c, labels } : c));
          ok++;
        } catch { fail++; }
        done++;
        onProgress && onProgress(done, todo.length);
      }
    };
    await Promise.all(Array.from({ length: Math.min(4, todo.length) }, worker));
    return { total: todo.length, ok, fail };
  }

  // ── Sincronizar ───────────────────────────────────────────────────────────
  async function syncAll(manual = false) {
    if (isSyncing) return;
    const say = (title, body) => manual && window.dispatchEvent(new CustomEvent('horos:toast', {
      detail: { module: 'system', title, body } }));
    const tok = window.horosToken ? await window.horosToken().catch(() => null) : window.HOROS_SESSION?.access_token;
    if (!tok) { say('Não foi possível sincronizar', 'Sessão expirada. Recarregue a página.'); return; }
    setIsSyncing(true);
    try {
      // Timeout de 90s: sync geral pode levar tempo com muitas conversas
      const _mainCtrl = new AbortController();
      const _mainTimer = setTimeout(() => _mainCtrl.abort(), 90000);
      const r = await fetch('/api/whatsapp-sync', {
        headers: { 'Authorization': `Bearer ${tok}` },
        cache: 'no-store',
        signal: _mainCtrl.signal,
      }).catch(() => null).finally(() => clearTimeout(_mainTimer));
      if (!r?.ok) {
        const d = r ? await r.clone().json().catch(() => ({})) : {};
        say('Não foi possível sincronizar', d.error || (r ? `O servidor respondeu ${r.status}. Tente de novo.`
          : 'O WhatsApp demorou para responder. Tente de novo em instantes.'));
      } else {
        const d = await r.clone().json().catch(() => ({}));
        say('WhatsApp sincronizado', `${d.conversations ?? 0} conversas conferidas.`);
      }
      if (r?.ok) {
        // Resolve nomes via findContacts + fotos via findChats para conversas sem dados
        // Timeout individual de 15s em cada chamada secundária
        const _secCtrl = new AbortController();
        const _secTimer = setTimeout(() => _secCtrl.abort(), 15000);
        await Promise.all([
          fetch('/api/whatsapp-sync?contacts=1', { headers: { 'Authorization': `Bearer ${tok}` }, signal: _secCtrl.signal }),
          fetch('/api/whatsapp-sync?pics=1',     { headers: { 'Authorization': `Bearer ${tok}` }, signal: _secCtrl.signal }),
        ]).catch(() => {}).finally(() => clearTimeout(_secTimer));
        // Salva timestamp para throttle de 5 min (evita sync a cada page load)
        localStorage.setItem('horos-wa-last-sync', String(Date.now()));
        await loadConvs();
        // Recarrega mensagens da conversa aberta: mescla com estado atual (não substitui)
        // para preservar mensagens otimistas em trânsito e mensagens adicionadas via Realtime
        const cid = selectedIdRef.current;
        if (cid) {
          const { data } = await window.HOROS_SB.from('messages').select('*')
            .eq('conversation_id', cid).order('sent_at', { ascending: false, nullsFirst: false }).limit(200);
          if (data?.length && selectedIdRef.current === cid) {
            const mapped = data.map(_mapMsg).reverse();
            setMsgs(prev => {
              const existingIds = new Set(prev.map(m => m.id));
              const added = mapped.filter(m => !existingIds.has(m.id));
              const merged = added.length
                ? [...prev, ...added].sort((a, b) => new Date(a.sentAt) - new Date(b.sentAt))
                : prev;
              msgsCacheRef.current.set(cid, merged);
              return merged;
            });
          }
        }
      } else {
        console.error('[syncAll] HTTP', r?.status ?? 'timeout/abort');
      }
    } catch (e) {
      console.error('[syncAll]', e);
    } finally {
      setIsSyncing(false);
    }
  }

  // ── Auto-sync de nomes ao ativar filtro por etiqueta ─────────────────────
  // Se há contatos sem nome na etiqueta selecionada, dispara ?contacts=1 silencioso.
  const lastContactsSyncRef = React.useRef(0);
  React.useEffect(() => {
    if (!filter.startsWith('label:') || waStatus !== 'connected') return;
    const tok = window.HOROS_SESSION?.access_token;
    if (!tok) return;
    // Throttle: no máximo 1 vez a cada 3 min por sessão
    const now = Date.now();
    if (now - lastContactsSyncRef.current < 3 * 60 * 1000) return;
    const _fakeNames = new Set(['desconhecido', 'unknown', 'contato wa', 'contato', '']);
    const labelId = filter.slice(6);
    const labelConvs = convs.filter(c => c.labels.includes(labelId));
    const needsNameSync = labelConvs.some(c => {
      const n = (c.name || '').trim().toLowerCase();
      return !n || _fakeNames.has(n) || /^\d{8,}$/.test(n) || n.startsWith('(');
    });
    const needsPicSync = labelConvs.some(c => !c.pic && !(c.jid || '').endsWith('@g.us'));
    if (!needsNameSync && !needsPicSync) return;
    lastContactsSyncRef.current = now;
    if (needsNameSync) {
      // Nomes e fotos ausentes → sync completo (findContacts + findChats + backfill)
      fetch('/api/whatsapp-sync?contacts=1', { headers: { 'Authorization': `Bearer ${tok}` } })
        .then(r => r.ok && r.json())
        .then(result => { if ((result?.updated || 0) > 0) loadConvs(); })
        .catch(() => {});
    } else {
      // Só fotos ausentes → sync rápido de fotos via findChats
      fetch('/api/whatsapp-sync?pics=1', { headers: { 'Authorization': `Bearer ${tok}` } })
        .then(r => r.ok && r.json())
        .then(result => { if ((result?.updated || 0) > 0) loadConvs(); })
        .catch(() => {});
    }
  }, [filter, waStatus]);

  // ── Drag-and-drop de arquivos no chat ────────────────────────────────────
  function handleDragEnter(e) {
    e.preventDefault();
    if (!selectedId || !connected) return;
    if ([...e.dataTransfer.types].includes('Files')) {
      dragCounter.current++;
      setDragOver(true);
    }
  }
  function handleDragLeave(e) {
    e.preventDefault();
    dragCounter.current = Math.max(0, dragCounter.current - 1);
    if (dragCounter.current === 0) setDragOver(false);
  }
  function handleDragOver(e) { e.preventDefault(); }
  function handleDrop(e) {
    e.preventDefault();
    setDragOver(false);
    dragCounter.current = 0;
    if (!selectedId || !connected) return;
    const files = Array.from(e.dataTransfer.files);
    if (!files.length) return;
    chatInputRef.current?.addFiles(files);
  }

  // Falha no envio: mostra o motivo (API oficial explica janela de 24 h, formato, bloqueio…)
  async function _cloudSendFailed(r) {
    const d = await r.clone().json().catch(() => ({}));
    if (d.kind === 'window_closed') {
      setConvs(prev => prev.map(c => c.id === selectedIdRef.current ? { ...c, lastInboundAt: null } : c));
    }
    if (d.error && (waProvider === 'cloud' || d.kind)) {
      window.dispatchEvent(new CustomEvent('horos:toast', { detail: { module: 'system', title: 'Mensagem não enviada', body: d.error } }));
    }
  }

  // ── Envio de mensagem ─────────────────────────────────────────────────────
  async function handleSend(text) {
    if (!text || !selectedId) return;
    const optId = 'opt-' + (++sendOptSeqRef.current);
    const optPayload = replyTo ? { quoted: { fromMe: replyTo.fromMe, body: replyTo.body || '[mídia]' } } : {};
    const opt = { id: optId, waId: null, convId: selectedId, fromMe: true, type: 'text',
      body: text, status: 'sent', sentAt: new Date().toISOString(), payload: optPayload, reactions: [] };
    setMsgs(prev => [...prev, opt]);
    const { data: { session: freshSession } } = await window.HOROS_SB.auth.getSession();
    const tok = freshSession?.access_token || window.HOROS_SESSION?.access_token;
    try {
      const r = await fetch('/api/whatsapp-send', {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${tok}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ conversationId: selectedId, text, ...(replyTo ? { replyTo: { msgId: replyTo.waId, fromMe: replyTo.fromMe, body: replyTo.body || '[mídia]', type: replyTo.type || 'text' } } : {}) }),
      });
      if (!r.ok) {
        setMsgs(prev => prev.map(m => m.id === optId ? { ...m, status: 'failed' } : m));
        _cloudSendFailed(r);
      } else {
        localStorage.setItem('horos-tour-sent-msg', '1');
        window.dispatchEvent(new CustomEvent('horos:tour-check', { detail: { step: 'message' } }));
        const json = await r.json().catch(() => ({}));
        if (json.messageId) setMsgs(prev => prev.map(m => m.id === optId ? { ...m, waId: json.messageId } : m));
      }
    } catch {
      setMsgs(prev => prev.map(m => m.id === optId ? { ...m, status: 'failed' } : m));
    }
    setReplyTo(null);
  }

  // file e previewUrl são passados quando o arquivo ainda não foi lido como base64
  async function handleSendMedia({ mediaType, base64, file, previewUrl, mimetype, fileName, caption }) {
    if (!selectedId) return;
    const optId = 'opt-' + (++sendOptSeqRef.current);
    const type = mediaType === 'audio' ? 'audio' : mediaType === 'document' ? 'document' : mediaType === 'video' ? 'video' : 'image';
    // Preview imediato: blob URL (antes do FileReader) ou base64 já disponível
    const immediatePreview = type !== 'document' ? (previewUrl || (base64 ? base64 : null)) : null;
    const opt = { id: optId, waId: null, convId: selectedId, fromMe: true, type,
      body: caption || '', status: 'sent', sentAt: new Date().toISOString(),
      payload: { fileName, ...(immediatePreview ? { localBase64: immediatePreview, localMime: mimetype } : {}) }, reactions: [] };
    setMsgs(prev => [...prev, opt]);
    // Arquivo → base64 (pequeno) ou caminho no Storage (grande / resposta rápida) — horosWaMediaBody
    let mediaBody = base64 ? { base64 } : null;
    if (!mediaBody && file) {
      try { mediaBody = await window.horosWaMediaBody(file); }
      catch (e) {
        setMsgs(prev => prev.map(m => m.id === optId ? { ...m, status: 'failed' } : m));
        window.dispatchEvent(new CustomEvent('horos:toast', { detail: { module: 'system', title: 'Arquivo não enviado', body: e.message || 'Tente de novo.' } }));
        return;
      }
    }
    const { data: { session: freshSession } } = await window.HOROS_SB.auth.getSession();
    const tok = freshSession?.access_token || window.HOROS_SESSION?.access_token;
    try {
      const r = await fetch('/api/whatsapp-send', {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${tok}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ conversationId: selectedId, mediaType, ...mediaBody, mimetype, fileName, caption }),
      });
      if (!r.ok) {
        setMsgs(prev => prev.map(m => m.id === optId ? { ...m, status: 'failed' } : m));
        _cloudSendFailed(r);
      } else {
        const json = await r.json().catch(() => ({}));
        if (json.messageId) setMsgs(prev => prev.map(m => m.id === optId ? { ...m, waId: json.messageId } : m));
      }
    } catch {
      setMsgs(prev => prev.map(m => m.id === optId ? { ...m, status: 'failed' } : m));
    }
  }

  async function handleReact(msg, emoji) {
    if (!selectedId || !msg.waId) return;
    const tok = window.HOROS_SESSION?.access_token;
    // BUG 6a FIX: detect if user is toggling off their own reaction
    const isMine = r => r.sender === 'me' || r.from === 'me';
    const existingReaction = (msg.reactions || []).find(r => isMine(r) && r.emoji === emoji);
    // If clicking own existing reaction, remove it (send empty emoji to WA)
    const sendEmoji = existingReaction ? '' : emoji;
    setMsgs(prev => prev.map(m => {
      if (m.id !== msg.id) return m;
      const reactions = existingReaction
        ? m.reactions.filter(r => !(isMine(r) && r.emoji === emoji))
        : [...(m.reactions || []), { sender: 'me', emoji }];
      return { ...m, reactions };
    }));
    fetch('/api/whatsapp-send', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${tok}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ conversationId: selectedId, reactionTo: { msgId: msg.waId, fromMe: msg.fromMe }, emoji: sendEmoji }),
    }).catch(() => {});
  }

  // ── Keyboard shortcuts globais (R, Esc) ───────────────────────────────────
  React.useEffect(() => {
    function h(e) {
      const tag = document.activeElement?.tagName;
      const inInput = tag === 'INPUT' || tag === 'TEXTAREA' || document.activeElement?.isContentEditable;

      // R → responder à última mensagem recebida (só quando conversa aberta e não dentro de input)
      if (e.key === 'r' && !inInput && !e.ctrlKey && !e.metaKey && !e.altKey && selectedId && msgs.length > 0) {
        e.preventDefault();
        const lastMsg = [...msgs].reverse().find(m => !m.fromMe) || msgs[msgs.length - 1];
        if (lastMsg) setReplyTo(lastMsg);
        return;
      }

      // Esc → fecha modais em cascata, deseleciona conversa
      if (e.key === 'Escape' && !inInput) {
        if (chatInputRef.current?.isPickerActive?.()) return; // Esc propagated from OS file dialog
        if (showNewConv) { setShowNewConv(false); setNewConvPhone(''); return; }
        if (drawer)      { setDrawer(null); return; }
        if (forwardMsg)  { setForwardMsg(null); return; }
        if (replyTo)     { setReplyTo(null); return; }
        if (selectedId)  { setSelectedId(null); return; }
      }
    }
    document.addEventListener('keydown', h);
    return () => document.removeEventListener('keydown', h);
  }, [selectedId, msgs, showNewConv, drawer, forwardMsg, replyTo]);

  // ── Nova conversa ─────────────────────────────────────────────────────────
  async function handleCreateConv({ name, phone, ddi = '55' }) {
    const digits = (phone || '').replace(/\D/g, '');
    const fullNumber = ddi + digits;
    const jid = fullNumber + '@s.whatsapp.net';
    const cleanName = String(name || '').trim();
    const sb = window.HOROS_SB;
    // mesmo número com ou sem 55
    const keyOf = (v) => { const d = String(v || '').replace(/\D/g, ''); return d.length > 11 && d.startsWith('55') ? d.slice(2) : d; };
    const existing = convs.find(c => c.jid === jid || (keyOf(c.phone) && keyOf(c.phone) === keyOf(fullNumber)));

    // Nome salvo pelo usuário fica travado no banco (conversations.name_locked):
    // as sincronizações com o WhatsApp não sobrescrevem mais
    const saveName = async (convId) => {
      if (!cleanName) return;
      const { error: upErr } = await sb.from('conversations').update({ contact_name: cleanName }).eq('id', convId);
      if (!upErr) setConvs(prev => prev.map(c => c.id === convId ? { ...c, name: cleanName, unnamed: false } : c));
    };

    if (existing) {
      await saveName(existing.id);
      selectConv(existing.id);
      setShowNewConv(false);
      return;
    }

    // BUG 3 FIX: ensure user_id is always set — RLS requires it
    const uid = window.horosActUid?.() || window.HOROS_USER?.id;
    if (!uid) throw new Error('Usuário não autenticado');
    let { data, error } = await sb.from('conversations').insert({
      user_id: uid, wa_jid: jid, contact_phone: digits, contact_name: cleanName || null,
    }).select().single();
    if (error && String(error.code) === '23505') {
      // já existia no banco (fora da lista carregada): usa a conversa existente e grava o nome
      const { data: row } = await sb.from('conversations').select('*').eq('user_id', uid).eq('wa_jid', jid).maybeSingle();
      if (row) {
        await saveName(row.id);
        data = { ...row, contact_name: cleanName || row.contact_name };
        error = null;
      }
    }
    if (error) throw error;
    const newConv = _mapConv(data);
    setConvs(prev => prev.some(c => c.id === newConv.id) ? prev.map(c => c.id === newConv.id ? newConv : c) : [newConv, ...prev]);
    setShowNewConv(false);
    selectConv(newConv.id);

    // Salva contato na Evolution API (fire-and-forget)
    if (name) {
      const tok = window.HOROS_SESSION?.access_token;
      fetch('/api/whatsapp-instance', {
        method: 'PUT',
        headers: { 'Authorization': `Bearer ${tok}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ number: fullNumber, contactName: '[Horos] ' + name }),
      }).catch(() => {});
    }
  }

  function onLeadCreated(newLead) {
    setLead(newLead);
    setLeads(prev => [newLead, ...prev]);
  }

  // ── Deletar mensagem ──────────────────────────────────────────────────────
  async function handleDelete(msg, forAll) {
    const tok = window.HOROS_SESSION?.access_token;
    const deletedBody = forAll ? 'Mensagem apagada' : null;
    const deletedType = forAll ? 'deleted' : 'deleted_local';
    // Optimistic update
    setMsgs(prev => prev.map(m => m.id === msg.id
      ? { ...m, body: deletedBody || '[apagada]', type: deletedType }
      : m
    ));
    // Precisa do waId para revogar no WA
    if (!msg.waId) { setMsgs(prev => prev.map(m => m.id === msg.id ? msg : m)); return; }
    try {
      const r = await fetch('/api/whatsapp-send', {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${tok}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ conversationId: selectedId, deleteMsg: { msgId: msg.waId, fromMe: msg.fromMe, jid: selectedConvRef.current?.jid }, forAll }),
      });
      if (!r.ok) {
        setMsgs(prev => prev.map(m => m.id === msg.id ? msg : m));
      }
    } catch {
      setMsgs(prev => prev.map(m => m.id === msg.id ? msg : m));
    }
  }

  // ── Encaminhar mensagem ───────────────────────────────────────────────────
  // Mídia (foto, vídeo, áudio, documento, figurinha) é baixada e reenviada como arquivo — antes ia só o texto
  async function handleForward(msg, targetConvId) {
    const tok = window.horosToken ? await window.horosToken() : window.HOROS_SESSION?.access_token;
    const say = (title, body) => window.dispatchEvent(new CustomEvent('horos:toast', { detail: { module: 'messages', title, body } }));
    const PH = { '[Imagem]': 'image', '[Image]': 'image', '[Sticker]': 'image', '[Vídeo]': 'video', '[Video]': 'video', '[Áudio]': 'audio', '[Audio]': 'audio' };
    const t = msg.type === 'text' && PH[msg.body] ? PH[msg.body] : msg.type;
    const mediaType = t === 'image' || t === 'sticker' ? 'image' : t === 'video' ? 'video'
      : t === 'audio' || t === 'ptt' ? 'audio' : t === 'document' || t === 'doc' ? 'document' : null;
    const send = (body) => fetch('/api/whatsapp-send', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${tok}`, 'Content-Type': 'application/json' },
      body: JSON.stringify({ conversationId: targetConvId, ...body }),
    });
    try {
      let r;
      if (mediaType) {
        let b64 = null, mime = null;
        if (msg.payload?.localBase64) { b64 = msg.payload.localBase64; mime = msg.payload.localMime; }
        else if (msg.waId) {
          const mr = await fetch(`/api/whatsapp-media?msgId=${encodeURIComponent(msg.waId)}`, { headers: { 'Authorization': `Bearer ${tok}` } });
          const d = mr.ok ? await mr.json().catch(() => ({})) : {};
          b64 = d.base64 || null; mime = d.mimetype || null;
          if (!b64 && d.url) {                      // arquivo grande: baixa pelo link e reenvia pelo Storage
            const blob = await fetch(d.url).then(x => x.ok ? x.blob() : null).catch(() => null);
            if (blob) {
              const fn = msg.payload?.fileName || (mediaType === 'document' ? (msg.body || 'arquivo') : 'arquivo');
              const mb = await window.horosWaMediaBody(new File([blob], fn, { type: mime || blob.type }));
              const isPh0 = !msg.body || PH[msg.body] || (mediaType === 'document' && msg.body === msg.payload?.fileName);
              r = await send({ mediaType, ...mb, mimetype: mime || blob.type, fileName: msg.payload?.fileName, caption: isPh0 || mediaType === 'audio' ? '' : msg.body });
              if (!r.ok) { const e = await r.json().catch(() => ({})); say('Não foi possível encaminhar', e.error || 'Tente de novo.'); return; }
              const to0 = convs.find(c => c.id === targetConvId);
              say('Mensagem encaminhada', to0?.name ? 'Para ' + to0.name : ''); return;
            }
          }
        }
        if (b64 && /^blob:/.test(b64)) b64 = null;
        if (!b64) { say('Não foi possível encaminhar', 'O arquivo não está mais disponível no WhatsApp.'); return; }
        const m = /^data:([^;]+);base64,/.exec(b64);
        if (m) { mime = mime || m[1]; b64 = b64.slice(m[0].length); }
        mime = mime || (mediaType === 'image' ? 'image/jpeg' : mediaType === 'video' ? 'video/mp4' : mediaType === 'audio' ? 'audio/ogg' : 'application/octet-stream');
        const isPh = !msg.body || PH[msg.body] || (mediaType === 'document' && msg.body === msg.payload?.fileName);
        const fileName = msg.payload?.fileName || (mediaType === 'document' ? (msg.body || 'arquivo') : undefined);
        let mb = { base64: b64 };
        if (b64.length > 3400000) {                 // passa do limite do corpo da Vercel → sobe antes
          const bin = atob(b64); const arr = new Uint8Array(bin.length);
          for (let i = 0; i < bin.length; i++) arr[i] = bin.charCodeAt(i);
          mb = await window.horosWaMediaBody(new File([arr], fileName || 'arquivo', { type: mime }));
        }
        r = await send({ mediaType, ...mb, mimetype: mime, fileName, caption: isPh || mediaType === 'audio' ? '' : msg.body });
      } else {
        if (!msg.body) return;
        r = await send({ text: msg.body });
      }
      if (!r.ok) { const d = await r.json().catch(() => ({})); say('Não foi possível encaminhar', d.error || 'Tente de novo.'); return; }
      const to = convs.find(c => c.id === targetConvId);
      say('Mensagem encaminhada', to?.name ? 'Para ' + to.name : '');
    } catch {
      say('Não foi possível encaminhar', 'Verifique a conexão e tente de novo.');
    }
  }

  // ── Pin / Archive / MarkUnread ────────────────────────────────────────────
  async function handlePin(convId, pinned) {
    setConvs(prev => prev.map(c => c.id === convId ? { ...c, pinned } : c));
    await window.HOROS_SB.from('conversations').update({ pinned }).eq('id', convId).then(() => {}, () => {});
  }

  async function handleArchive(convId, archived = true) {
    const conv = convs.find(c => c.id === convId);
    setConvs(prev => prev.map(c => c.id === convId ? { ...c, archived } : c));
    await window.HOROS_SB.rpc('archive_conversation', { p_id: convId, p_archived: archived });
    if (archived && selectedId === convId) { setSelectedId(null); setMsgs([]); }
    const tok = window.HOROS_SESSION?.access_token;
    if (tok && conv?.jid) {
      fetch('/api/whatsapp-instance', {
        method: 'PUT',
        headers: { 'Authorization': `Bearer ${tok}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({ archiveChat: { jid: conv.jid, archive: archived } }),
      }).catch(() => {});
    }
  }

  // Atualiza o lead no painel E na lista em memória — antes só o painel mudava, e ao voltar
  // para a conversa o lead vinha da lista com a etapa antiga
  function patchLead(leadId, fields) {
    setLead(l => l?.id === leadId ? { ...l, ...fields } : l);
    setLeads(prev => prev.map(l => l.id === leadId ? { ...l, ...fields } : l));
  }

  // Etapa mudada em outra parte da tela (ficha embutida, Horos 360) também atualiza aqui
  React.useEffect(() => {
    const onUpd = e => {
      const d = e.detail || {};
      if (!d.id) return;
      const f = {};
      for (const k of ['col', 'name', 'temp', 'score', 'phone']) if (d[k] !== undefined) f[k] = d[k];
      if (Object.keys(f).length) patchLead(d.id, f);
    };
    window.addEventListener('horos:lead-updated', onUpd);
    return () => window.removeEventListener('horos:lead-updated', onUpd);
  }, []);

  async function handleResolveLead(leadId) {
    patchLead(leadId, { col: 'fechado' });
    await window.HOROS_SB.rpc('move_lead', { p_id: leadId, p_col: 'fechado' }).then(({ error, data }) => {
      if (!error && data !== false) horosAskSale(leadId);
    }, () => {});
  }

  async function handleMarkUnread(convId, markUnread) {
    const newUnread = markUnread ? 1 : 0;
    setConvs(prev => prev.map(c => c.id === convId ? { ...c, unread: newUnread } : c));
    if (markUnread) {
      _CLEARED_CONVS.delete(convId);
      setReadConvIds(prev => {
        const n = new Set(prev); n.delete(convId);
        try { localStorage.setItem('horos-read-convs', JSON.stringify([...n])); } catch {}
        return n;
      });
    }
    await window.HOROS_SB.from('conversations').update({ unread_count: newUnread }).eq('id', convId).then(() => {}, () => {});
  }

  const selectedConvRef = React.useRef(null);
  const selectedConv = convs.find(c => c.id === selectedId) || null;
  // WhatsApp oficial: fora da janela de 24 h (ou grupo) o campo vira o aviso + "Enviar modelo"
  const cloudBlocked = waProvider === 'cloud' && waStatus === 'connected' && !!selectedConv
    && (selectedConv.jid.endsWith('@g.us') || !_cloudWindowOpen(selectedConv));
  React.useEffect(() => { selectedConvRef.current = selectedConv; }, [selectedConv]);
  const connected = waStatus === 'connected';

  // Conversas para exibição na lista: convs lidas pelo usuário sempre aparecem com unread=0
  // (independente do que o banco ou Realtime devolva enquanto o usuário não receber msg nova)
  const convsForList = React.useMemo(() =>
    readConvIds.size === 0 ? convs
      : convs.map(c => readConvIds.has(c.id) ? { ...c, unread: 0 } : c),
  [convs, readConvIds]);

  React.useEffect(() => {
    localStorage.setItem('horos-wa-connected', connected ? '1' : '0');
    if (connected && !explicitDisconnRef.current) {
      localStorage.removeItem('horos-wa-user-disconnected');
      setUserDidDisconnect(false);
    }
  }, [connected]);
  // Mostra lista APENAS quando conectado — desconectado mostra só a tela de conexão
  const showConvList = connected;

  return (
    <div className="app">
      <Sidebar active="messages" />
      <div className="main">
        <Topbar title="Mensagens" theme={theme} onThemeChange={setTheme} />
        <div className="content msg-content">
          {waBanInfo && !connected && !userDidDisconnect && (
            <_WaBanBanner info={waBanInfo} onDismiss={() => setWaBanInfo(null)} />
          )}
          <div className={`mc-shell${selectedConv ? ' has-conv' : ''}${selectedConv && panelOpen ? ' panel-open' : ''}`} style={
            !showConvList
              ? { gridTemplateColumns: '1fr' }
              : !selectedConv
              ? { gridTemplateColumns: '360px minmax(0,1fr)' }
              : !panelOpen
              ? { gridTemplateColumns: '360px minmax(0,1fr) 72px' }
              : undefined
          }>

            {/* Left — Lista de conversas */}
            {showConvList && (
              <div className="mc-list-wrap">
                <_ConvList
                  convs={convsForList} leads={leads} selected={selectedId}
                  filter={filter} search={search} waLabels={waLabels}
                  labelVisible={labelVisible}
                  convsLoading={convsLoading}
                  onSelect={selectConv}
                  onFilter={setFilter}
                  onSearch={setSearch}
                  onNewConv={() => { setNewConvPhone(''); setShowNewConv(true); }}
                  onSync={() => syncAll(true)} isSyncing={isSyncing}
                  onDisconnect={connected ? disconnectWa : null}
                  onSelectReply={r => setQuickReplyText(r)}
                  onPin={handlePin}
                  onArchive={handleArchive}
                  onMarkUnread={handleMarkUnread}
                  onResolve={handleResolveLead}
                  readConvIds={readConvIds}
                  onBulkLabel={connected ? bulkLabel : null}
                  onAddConv={c => setConvs(prev => prev.some(x => x.id === c.id) ? prev : [...prev, c])}
                  onLidLink={connected ? (id => { selectConv(id); setPanelOpen(true); }) : null}
                />
                {showNewConv && (
                  <_NewConvModal
                    onClose={() => { setShowNewConv(false); setNewConvPhone(''); }}
                    onCreate={handleCreateConv}
                    initialPhone={newConvPhone} />
                )}
              </div>
            )}

            {/* Center — Chat ou tela de conexão */}
            {!showConvList ? (
              // Sem histórico e completamente desconectado → tela de conexão full
              <_ConnectScreen phase={waStatus} qr={qrData}
                onConnect={connectWa} onDisconnect={disconnectWa} onRefreshQr={refreshQr} waPhone={waPhone} />
            ) : (
            <div className="mc-chat" style={{ position: 'relative' }}
              onDragEnter={handleDragEnter}
              onDragLeave={handleDragLeave}
              onDragOver={handleDragOver}
              onDrop={handleDrop}>
              {dragOver && (
                <div style={{
                  position: 'absolute', inset: 0, zIndex: 900,
                  background: 'rgba(31,147,255,0.08)',
                  border: '2px dashed var(--accent)',
                  borderRadius: 8,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  pointerEvents: 'none',
                  flexDirection: 'column', gap: 12,
                }}>
                  <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
                  </svg>
                  <span style={{ fontSize: 16, fontWeight: 600, color: 'var(--accent)', letterSpacing: 0.1 }}>
                    Arraste arquivo aqui
                  </span>
                </div>
              )}
              {!selectedId ? (
                !connected ? (
                  <_ConnectScreen
                    phase={waStatus === 'connected' ? 'connected' :
                           (waStatus === 'qr' || waStatus === 'connecting') ? waStatus :
                           (waStatus === 'loading' && convs.length === 0 ? 'loading' : 'idle')}
                    qr={qrData} onConnect={connectWa} onDisconnect={disconnectWa} onRefreshQr={refreshQr} waPhone={waPhone} />
                ) : (
                  <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center',
                    flexDirection: 'column', gap: 12, color: 'var(--fg-4)' }}>
                    <div style={{ opacity: 0.4 }}><MIcon name="msgs" size={40} /></div>
                    <div style={{ fontSize: 15 }}>Selecione uma conversa</div>
                  </div>
                )
              ) : (
                <>
                  <_ChatHeader conv={selectedConv} lead={lead} waLabels={waLabels}
                    panelOpen={panelOpen} onTogglePanel={() => setPanelOpen(v => !v)} onOpenPanel={() => setPanelOpen(true)}
                    isSyncingConv={isSyncingConv} onAction={type => setActionModal({ type })}
                    onSync={syncConv}
                    onToggleLabel={connected && waProvider !== 'cloud' ? (labelId, active) => toggleLabel(selectedConv?.id, labelId, active) : null}
                    onBack={() => { setPanelOpen(false); setSelectedId(null); }} />
                  <_ChatBody msgs={msgs} onReact={handleReact} onReply={setReplyTo}
                    onDelete={handleDelete} onForward={msg => setForwardMsg(msg)}
                    conv={selectedConv} convs={convs} loading={loadingMsgs} bottomRef={bottomRef}
                    hasMore={hasMore} loadingMore={loadingMore} onLoadMore={loadMoreMsgs} />
                  {!connected && (
                    <div style={{
                      padding: '12px 20px', background: 'var(--bg-2)',
                      borderTop: '1px solid var(--border)',
                      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
                      gap: 12, flexShrink: 0,
                    }}>
                      <span style={{ fontSize: 13, color: 'var(--fg-3)' }}>
                        WhatsApp desconectado — não é possível enviar mensagens
                      </span>
                      <button onClick={() => { setSelectedId(null); connectWa(); }} style={{
                        background: 'var(--accent)', color: '#fff', border: 'none',
                        borderRadius: 8, padding: '7px 16px', fontSize: 13,
                        fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
                      }}>
                        Reconectar
                      </button>
                    </div>
                  )}
                </>
              )}
              {/* _ChatInput always mounted — pending files never lost due to parent re-renders */}
              {copilotOn && selectedConv && connected && window._HiSuggestionBar && React.createElement(window._HiSuggestionBar, {
                conv:        selectedConv,
                suggestion:  suggestion,
                intellStatus: selectedConv?.intellStatus,
                onApproved: () => setSuggestion(s => s ? { ...s, status: 'APPROVED', effective_status: 'APPROVED' } : s),
                onRejected: () => setSuggestion(s => s ? { ...s, status: 'REJECTED', effective_status: 'REJECTED' } : s),
              })}
              {cloudBlocked && <_CloudWindowBar isGroup={selectedConv.jid.endsWith('@g.us')} onOpen={() => setTplModal(true)} />}
              {tplModal && selectedConv && <_CloudTemplateModal conv={selectedConv} onClose={() => setTplModal(false)} />}
              <div style={{ display: selectedId && !cloudBlocked ? undefined : 'none' }}>
                <_ChatInput ref={chatInputRef} convId={selectedId} onSend={handleSend} onSendMedia={handleSendMedia}
                  replyTo={replyTo} onCancelReply={() => setReplyTo(null)} disabled={!selectedId || !connected}
                  prefillText={quickReplyText} onPrefillConsumed={() => setQuickReplyText('')}
                  contactName={selectedConv?.name}
                  assOn={copilotOn} onToggleAss={toggleCopilot} />
              </div>
            </div>
            )}

            {/* Right — Painel CRM aberto. Só com o layout de conversa (WA conectado): desconectado a
                grade tem 1 coluna e o painel caía embaixo da tela "Conectar WhatsApp" */}
            {showConvList && selectedConv && panelOpen && (
              <button type="button" className="mc-rp-mclose" aria-label="Fechar painel" onClick={() => setPanelOpen(false)}>
                <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
              </button>
            )}
            {showConvList && selectedConv && panelOpen && (
              <_RightPanel conv={selectedConv} lead={lead} waLabels={waLabels} waPhone={waPhone}
                onLeadCreated={onLeadCreated}
                onOpenDrawer={id => setDrawer(id)}
                onMoveCol={(leadId, col, pipeline_id) => patchLead(leadId, pipeline_id === undefined ? { col } : { col, pipeline_id })}
                onToggleLabel={connected && waProvider !== 'cloud' ? (labelId, active) => toggleLabel(selectedConv?.id, labelId, active) : null}
                suggestion={suggestion}
                onSuggestionChange={setSuggestion}
                onAction={type => setActionModal({ type })}
                onRenameConv={(convId, newName) => setConvs(cs => cs.map(c => c.id === convId ? { ...c, name: newName } : c))}
                onLidLinked={async (convId) => { await loadConvs(); selectConv(convId); }}
                copilotOn={copilotOn} />
            )}
            {/* Right — Pílula colapsada */}
            {showConvList && selectedConv && !panelOpen && (
              <_PanelPill conv={selectedConv} lead={lead}
                onToggle={() => setPanelOpen(true)}
                onOpenDrawer={id => setDrawer(id)}
                onAction={type => setActionModal({ type })} />
            )}
          </div>

          {drawer && (
            <_ClientDrawer leadId={drawer} onClose={() => setDrawer(null)} />
          )}
          {forwardMsg && (
            <_ForwardModal msg={forwardMsg} convs={convs}
              onClose={() => setForwardMsg(null)}
              onForward={(msg, targetConvId) => { handleForward(msg, targetConvId); setForwardMsg(null); }} />
          )}
          {copilotIntro && <_CopilotIntroModal onClose={closeCopilotIntro} />}
          {actionModal && (
            <_QuickActionModal type={actionModal.type} conv={selectedConv} lead={lead}
              onClose={() => setActionModal(null)} />
          )}
        </div>
      </div>
    </div>
  );
}
