// Disparo em Massa (botão no Dashboard e no CRM). Novo disparo = assistente de 4 passos com passos na lateral (BcWizard)
// Abre com: window.dispatchEvent(new CustomEvent('horos:open-broadcasts'))
// Banco: broadcasts / broadcast_recipients (RLS: só o dono) + RPCs broadcast_audience,
// create_broadcast, set_broadcast_status. O envio roda no servidor (/api/broadcast-worker,
// pg_cron a cada minuto) — pode fechar a tela que continua.

const BC_STATUS = {
  scheduled: { label: 'Agendado',  cls: 'sched' },
  running:   { label: 'Enviando',  cls: 'run' },
  paused:    { label: 'Pausado',   cls: 'pause' },
  done:      { label: 'Concluído', cls: 'done' },
  canceled:  { label: 'Cancelado', cls: 'cancel' },
};
const BC_PACE = [
  { id: 'safe',   label: 'Seguro',  min: 45, max: 120, hint: 'Recomendado para números novos ou listas frias' },
  { id: 'normal', label: 'Normal',  min: 30, max: 90,  hint: 'Bom equilíbrio entre velocidade e segurança' },
  { id: 'fast',   label: 'Rápido',  min: 15, max: 45,  hint: 'Só para contatos que já conversam com você' },
];
const _BC_WA_COLORS = ['#ff9485','#64c4ff','#ffd429','#dfaef0','#99b6c1','#55ccb3','#ff9dff','#d3a91d','#6d7cce','#d7e752','#00d0e2','#ffc5c7','#93ceac','#f74848','#00a0f2','#83e422','#ffaf04','#b5ebff','#9ba6ff','#9368cf'];

function _bcFmtDate(ts) {
  if (!ts) return '—';
  const d = new Date(ts);
  return d.toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit' }) + ' ' + d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
}
function _bcDuration(sec) {
  if (sec < 3600) return `${Math.max(1, Math.round(sec / 60))} min`;
  const h = Math.floor(sec / 3600), m = Math.round((sec % 3600) / 60);
  return m ? `${h}h ${m}min` : `${h}h`;
}
function _bcPersonalize(text, name) {
  const full = String(name || '').trim(), first = full.split(/\s+/)[0] || '';
  return String(text || '')
    .replace(/\{\s*primeiro_nome\s*\}/gi, first).replace(/\{\s*nome\s*\}/gi, full)
    .replace(/\{([^{}]*\|[^{}]*)\}/g, (_, g) => g.split('|')[0])
    .replace(/[ \t]+([!?.,;:])/g, '$1').trim();
}
// "telefone, nome" / "nome;telefone" / colado do Excel (tab) — um por linha
function _bcParseList(raw) {
  return String(raw || '').split(/\r?\n/).map(line => {
    const parts = line.split(/[,;\t]/).map(s => s.trim()).filter(Boolean);
    const ph = parts.find(p => p.replace(/\D/g, '').length >= 10);
    if (!ph) return null;
    const name = parts.filter(p => p !== ph).join(' ').trim();
    return { phone: ph, name };
  }).filter(Boolean);
}

// Planilha (.xlsx/.xls/.csv) → [{ phone, name }]. Em cada linha: 1ª célula com 10+ dígitos = telefone,
// 1ª célula de texto sem número = nome. Cabeçalho e linhas sem telefone são ignorados.
function _bcLoadXlsx() {
  if (window.XLSX) return Promise.resolve();
  return new Promise((ok, fail) => {
    const s = document.createElement('script');
    s.src = 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js';
    s.onload = ok; s.onerror = () => fail(new Error('Não foi possível carregar o leitor de planilhas.'));
    document.head.appendChild(s);
  });
}
async function _bcReadSheet(file) {
  await _bcLoadXlsx();
  const rows = horosReadSheetRows(await file.arrayBuffer(), file.name, { raw: false });
  return rows.map(r => {
    const cells = r.map(c => String(c).trim()).filter(Boolean);
    const ph = cells.find(c => c.replace(/\D/g, '').length >= 10 && !/[a-zA-ZÀ-ÿ]{3,}/.test(c));
    if (!ph) return null;
    const name = cells.find(c => c !== ph && !/\d/.test(c) && !c.includes('@')) || '';
    return { phone: ph, name };
  }).filter(Boolean);
}

function BroadcastRoot() {
  const [open, setOpen] = React.useState(false);
  React.useEffect(() => {
    const on = () => setOpen(true);
    window.addEventListener('horos:open-broadcasts', on);
    return () => window.removeEventListener('horos:open-broadcasts', on);
  }, []);
  if (!open) return null;
  return <BroadcastPanel onClose={() => setOpen(false)} />;
}

function BroadcastPanel({ onClose = () => {}, pageMode = false }) {
  const [view, setView] = React.useState('list'); // list | new | detail
  const [list, setList] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [detail, setDetail] = React.useState(null);
  const [editing, setEditing] = React.useState(false);
  const openEdit = (b) => { setDetail(b); setView('detail'); setEditing(true); };
  const sb = window.HOROS_SB;
  // Disparos em massa: Pro e Max; só o plano Distribuição não tem (o banco também recusa — trg_broadcasts_plan_guard)
  const locked = useHorosPlan().tier === 'dist';

  const load = React.useCallback(async () => {
    const { data } = await sb.from('broadcasts').select('*').order('created_at', { ascending: false }).limit(100);
    setList(data || []);
    setLoading(false);
  }, []);

  React.useEffect(() => {
    load();
    const ch = sb.channel('bc-list-' + Math.random().toString(36).slice(2))
      .on('postgres_changes', { event: '*', schema: 'public', table: 'broadcasts' }, (p) => {
        if (p.eventType === 'DELETE') { const id = p.old && p.old.id; if (id) setList(prev => prev.filter(b => b.id !== id)); return; }
        setList(prev => {
          const i = prev.findIndex(b => b.id === p.new.id);
          if (i < 0) return [p.new, ...prev];
          const n = [...prev]; n[i] = { ...n[i], ...p.new }; return n;
        });
        setDetail(d => d && d.id === p.new.id ? { ...d, ...p.new } : d);
      })
      .subscribe();
    const poll = setInterval(load, 15000);
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => { sb.removeChannel(ch); clearInterval(poll); window.removeEventListener('keydown', onKey); };
  }, [load]);

  const setStatus = async (b, status) => {
    if (status === 'canceled' && !window.confirm(`Cancelar o disparo "${b.name}"? Os contatos que ainda não receberam não receberão.`)) return;
    const { error } = await sb.rpc('set_broadcast_status', { p_id: b.id, p_status: status });
    if (error) { alert('Não foi possível alterar: ' + error.message); return; }
    load();
  };

  // Apagar campanha: some da lista com a fila; mensagens já enviadas continuam nas conversas
  const removeBc = async (b) => {
    const { data, error } = await sb.rpc('delete_broadcast', { p_id: b.id });
    if (error || !data?.ok) { alert('Não foi possível apagar: ' + (error?.message || 'disparo não encontrado')); return; }
    if (data.media?.path) sb.storage.from('broadcast-media').remove([data.media.path]).catch(() => {});
    setList(prev => prev.filter(x => x.id !== b.id));
    if (detail && detail.id === b.id) { setView('list'); setDetail(null); setEditing(false); }
    window.dispatchEvent(new CustomEvent('horos:toast', { detail: { title: 'Disparo apagado', body: b.name } }));
  };

  const _bcInner = (
    <div className={`bc-box${pageMode ? ' bc-page-mode' : ''}`} role="dialog" aria-label="Disparos">
        <div className="bc-head">
          <div className="bc-head-title">
            {view === 'detail' && (
              <button className="bc-back" onClick={() => { setView('list'); setDetail(null); setEditing(false); }} title="Voltar">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="m15 18-6-6 6-6"/></svg>
              </button>
            )}
            <span className="bc-head-icon"><Icon name="send" size={18} stroke={1.8} /></span>
            <div>
              <div className="bc-title">{view === 'detail' ? (detail?.name || 'Disparo') : 'Disparo em Massa'}</div>
              <div className="bc-sub">{view === 'new' ? 'Envie mensagens para vários contatos de uma só vez.' : 'Envio pelo seu WhatsApp, com ritmo seguro — continua mesmo com a tela fechada'}</div>
            </div>
          </div>
          <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
            {view === 'list' && !locked && <button className="bc-btn primary" onClick={() => setView('new')}><Icon name="plus" size={13} stroke={2.2} /> Novo disparo</button>}
            {!pageMode && <button className="bc-close" onClick={onClose} title="Fechar (Esc)"><Icon name="close" size={16} /></button>}
          </div>
        </div>

        <div className={`bc-body${view === 'new' ? ' flush' : ''}${locked && view === 'list' ? ' hpg-on' : ''}`}>
          {view === 'list' && locked && (
            <HorosPlanOverlay title="Disparos em massa não fazem parte do plano Distribuição" desc="Envie uma mensagem para vários contatos de uma vez, com ritmo seguro." />
          )}
          {view === 'list' && (
            loading ? <SkelList rows={4} avatar={36} right pad="14px 4px" />
            : list.length === 0 ? (
              <div className="bc-empty">
                <div className="bc-empty-icon"><BcMegaphone size={28} /></div>
                <div className="bc-empty-title">Nenhum disparo ainda</div>
                <div className="bc-empty-text">Envie uma mensagem para vários contatos de uma vez: leads de uma etapa do funil, uma etiqueta do WhatsApp ou uma lista colada.</div>
                <button className="bc-btn primary" onClick={() => setView('new')}><Icon name="plus" size={13} stroke={2.2} /> Criar primeiro disparo</button>
              </div>
            ) : (
              <div className="bc-list">
                {list.map(b => <BcCard key={b.id} b={b} onStatus={setStatus} onEdit={openEdit} onDelete={removeBc} onOpen={() => { setDetail(b); setView('detail'); setEditing(false); }} />)}
              </div>
            )
          )}
          {view === 'new' && <BcWizard onDone={() => { setView('list'); load(); }} onCancel={() => setView('list')} />}
          {view === 'detail' && detail && <BcDetail b={detail} onStatus={setStatus} onDelete={removeBc} editing={editing} setEditing={setEditing} onSaved={load} />}
        </div>
    </div>
  );
  if (pageMode) return _bcInner;
  return <div className="bc-overlay" onMouseDown={e => { if (e.target === e.currentTarget) onClose(); }}>{_bcInner}</div>;
}

// Ícone do Disparos (foguete) — o mesmo do botão quadrado em Mensagens
function BcMegaphone({ size = 14 }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.9" 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>
  );
}

function BcProgress({ b }) {
  const done = (b.sent || 0) + (b.failed || 0);
  const pct = b.total ? Math.round(done / b.total * 100) : 0;
  return (
    <div className="bc-progress">
      <div className="bc-progress-bar">
        <div className="bc-progress-sent" style={{ width: `${b.total ? (b.sent / b.total * 100) : 0}%` }} />
        <div className="bc-progress-fail" style={{ width: `${b.total ? (b.failed / b.total * 100) : 0}%` }} />
      </div>
      <div className="bc-progress-nums">
        <span><b>{b.sent || 0}</b> enviados</span>
        {b.failed > 0 && <span className="fail"><b>{b.failed}</b> falharam</span>}
        <span className="muted">{done}/{b.total} · {pct}%</span>
      </div>
    </div>
  );
}

// Pausar = para e guarda a posição (Retomar continua só com quem faltou).
// Cancelar = encerra de vez, não dá para retomar — por isso pede confirmação no próprio botão.
function BcActions({ b, onStatus, onEdit, onDelete }) {
  const [askCancel, setAskCancel] = React.useState(false);
  const [askDelete, setAskDelete] = React.useState(false);
  React.useEffect(() => {
    if (!askCancel && !askDelete) return;
    const t = setTimeout(() => { setAskCancel(false); setAskDelete(false); }, 8000);
    return () => clearTimeout(t);
  }, [askCancel, askDelete]);
  const left = Math.max(0, (b.total || 0) - (b.sent || 0) - (b.failed || 0));
  if (askDelete) {
    const live = b.status === 'running' || b.status === 'scheduled' || b.status === 'paused';
    return (
      <div className="bc-actions bc-actions--ask" onClick={e => e.stopPropagation()}>
        <span className="bc-ask-txt">Apagar "{b.name}"?{live && left ? ` Para de enviar — ${left.toLocaleString('pt-BR')} não vão receber.` : ''} As mensagens já enviadas continuam nas conversas.</span>
        <button className="bc-btn sm" onClick={() => setAskDelete(false)}>Voltar</button>
        <button className="bc-btn sm danger" onClick={() => { setAskDelete(false); onDelete(b); }}>Apagar</button>
      </div>
    );
  }
  if (askCancel) {
    return (
      <div className="bc-actions bc-actions--ask" onClick={e => e.stopPropagation()}>
        <span className="bc-ask-txt">Cancelar de vez?{left ? ` ${left.toLocaleString('pt-BR')} não vão receber.` : ''} Para continuar depois, use Pausar.</span>
        <button className="bc-btn sm" onClick={() => setAskCancel(false)}>Voltar</button>
        <button className="bc-btn sm danger" onClick={() => { setAskCancel(false); onStatus(b, 'canceled'); }}>Cancelar disparo</button>
      </div>
    );
  }
  return (
    <div className="bc-actions" onClick={e => e.stopPropagation()}>
      {(b.status === 'running' || b.status === 'scheduled') && <button className="bc-btn sm" title="Para agora e continua de onde parou ao retomar" onClick={() => onStatus(b, 'paused')}>Pausar</button>}
      {onEdit && (b.status === 'paused' || b.status === 'scheduled' || b.status === 'running') && <button className="bc-btn sm" title={b.status === 'running' ? 'Mudar o ritmo e o volume de envio' : 'Mudar ritmo, volume, mensagem e quem está na fila'} onClick={() => onEdit(b)}><Icon name="pencil" size={12} /> Editar</button>}
      {b.status === 'paused' && <button className="bc-btn sm primary" title="Continua só com quem ainda não recebeu" onClick={() => onStatus(b, 'running')}>Retomar</button>}
      {!['done', 'canceled'].includes(b.status) && <button className="bc-btn sm danger" title="Encerra de vez — não dá para retomar" onClick={() => setAskCancel(true)}>Cancelar</button>}
      {onDelete && <button className="bc-btn sm bc-del" title="Apagar campanha" aria-label="Apagar campanha" onClick={() => setAskDelete(true)}><Icon name="trash" size={13} /></button>}
    </div>
  );
}

// Leads cujo número não tem WhatsApp são arquivados sozinhos (broadcast_archive_no_wa no banco)
function BcArchivedNote({ n }) {
  return (
    <div className="bc-arch-note">
      <Icon name="archive" size={14} stroke={1.8} />
      <span><b>{n.toLocaleString('pt-BR')} {n === 1 ? 'cliente arquivado' : 'clientes arquivados'}</b> por não {n === 1 ? 'ter' : 'terem'} WhatsApp no número. Gerentes podem recuperar em Clientes › Arquivados.</span>
    </div>
  );
}

function BcCard({ b, onStatus, onOpen, onEdit, onDelete }) {
  const st = BC_STATUS[b.status] || BC_STATUS.scheduled;
  const next = b.status === 'running' && b.next_send_at ? new Date(b.next_send_at) : null;
  return (
    <div className="bc-card" onClick={onOpen}>
      <div className="bc-card-top">
        <div style={{ minWidth: 0 }}>
          <div className="bc-card-name">{b.name}</div>
          <div className="bc-card-meta">
            Criado {_bcFmtDate(b.created_at)}
            {b.status === 'scheduled' && <> · começa {_bcFmtDate(b.scheduled_at)}</>}
            {b.finished_at && <> · terminou {_bcFmtDate(b.finished_at)}</>}
            {b.after_col && <> · depois vai para <b>{typeof kanbanColTitle === 'function' ? kanbanColTitle(b.after_col) : b.after_col}</b></>}
          </div>
        </div>
        <span className={`bc-chip ${st.cls}`}>{b.status === 'running' && <span className="bc-dot" />}{st.label}</span>
      </div>
      <div className="bc-card-msg">{b.media?.fileName && <span className="bc-file-tag"><Icon name={b.media.mediaType === 'image' ? 'image' : b.media.mediaType === 'audio' ? 'mic' : 'file'} size={11} /> {b.media.fileName}</span>}{b.message}</div>
      <BcProgress b={b} />
      {b.archived_count > 0 && <BcArchivedNote n={b.archived_count} />}
      <div className="bc-card-foot">
        <span className="bc-reason">
          {b.status_reason
            ? b.status_reason
            : next ? (next > new Date() ? `Próximo envio às ${next.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}` : 'Enviando…')
            : `Ritmo: ${b.min_delay}–${b.max_delay}s · até ${b.daily_limit}/dia · ${b.window_start}h–${b.window_end}h`}
        </span>
        <BcActions b={b} onStatus={onStatus} onEdit={onEdit} onDelete={onDelete} />
      </div>
    </div>
  );
}

function BcDetail({ b, onStatus, onDelete, editing, setEditing, onSaved }) {
  const [rows, setRows] = React.useState([]);
  const [filter, setFilter] = React.useState('all');
  const [reload, setReload] = React.useState(0);
  const sb = window.HOROS_SB;
  const canEdit = b.status === 'paused' || b.status === 'scheduled' || b.status === 'running';
  React.useEffect(() => { if (!canEdit && editing) setEditing(false); }, [canEdit]);
  React.useEffect(() => {
    let alive = true;
    const load = async () => {
      let q = sb.from('broadcast_recipients').select('id,phone,name,status,error,sent_at,lead_archived').eq('broadcast_id', b.id).order('id').limit(1000);
      if (filter !== 'all') q = q.eq('status', filter);
      const { data } = await q;
      if (alive) setRows(data || []);
    };
    load();
    const t = setInterval(load, 8000);
    return () => { alive = false; clearInterval(t); };
  }, [b.id, filter, reload]);
  const st = BC_STATUS[b.status] || BC_STATUS.scheduled;
  return (
    <div className="bc-detail">
      <div className="bc-detail-top">
        <span className={`bc-chip ${st.cls}`}>{b.status === 'running' && <span className="bc-dot" />}{st.label}</span>
        {b.status_reason && <span className="bc-reason">{b.status_reason}</span>}
        <div style={{ flex: 1 }} />
        <BcActions b={b} onStatus={onStatus} onDelete={onDelete} onEdit={editing ? null : () => setEditing(true)} />
      </div>
      <BcProgress b={b} />
      {b.archived_count > 0 && <BcArchivedNote n={b.archived_count} />}
      {editing && canEdit
        ? <BcEditPanel b={b} onClose={() => setEditing(false)} onChanged={() => { setReload(n => n + 1); onSaved && onSaved(); }} />
        : <div className="bc-bubble-wrap"><div className="bc-bubble">{b.message || <i>(só mídia)</i>}</div></div>}
      <div className="bc-seg">
        {[['all', 'Todos'], ['pending', 'Na fila'], ['sent', 'Enviados'], ['failed', 'Falharam']].map(([id, l]) => (
          <button key={id} className={filter === id ? 'on' : ''} onClick={() => setFilter(id)}>{l}</button>
        ))}
      </div>
      <div className="bc-table">
        {rows.map(r => (
          <div key={r.id} className="bc-tr">
            <span className="bc-td-name">{r.name || '—'}</span>
            <span className="bc-td-phone">{fmtPhone(r.phone)}</span>
            <span className={`bc-td-st ${r.status}`}>
              {r.status === 'sent' ? 'Enviado' : r.status === 'failed' ? 'Falhou' : r.status === 'sending' ? 'Enviando…' : 'Na fila'}
            </span>
            <span className="bc-td-info" title={r.error || ''}>{r.status === 'failed' ? <>{r.error}{r.lead_archived && <span className="bc-arch-tag">Lead arquivado</span>}</> : r.sent_at ? _bcFmtDate(r.sent_at) : ''}</span>
            {editing && canEdit && r.status === 'pending' && (
              <button className="bc-td-rm" title="Tirar da fila" onClick={async () => {
                const { error } = await sb.rpc('broadcast_trim_queue', { p_id: b.id, p_ids: [r.id] });
                if (error) { alert('Não foi possível tirar: ' + error.message); return; }
                setRows(prev => prev.filter(x => x.id !== r.id)); onSaved && onSaved();
              }}><Icon name="close" size={12} /></button>
            )}
          </div>
        ))}
        {rows.length === 0 && <div className="bc-empty small">Nenhum contato neste filtro.</div>}
      </div>
    </div>
  );
}

// Ritmo e volume (2026-10-11, pedido do usuário: "tem que dar pra editar o tempo do volume nos disparos"):
// intervalo entre mensagens, máximo por dia, horário e início (agendado). Vale também com o disparo enviando.
const _bcLocalInput = (ts) => {
  if (!ts) return '';
  const d = new Date(ts); const z = (n) => String(n).padStart(2, '0');
  return `${d.getFullYear()}-${z(d.getMonth() + 1)}-${z(d.getDate())}T${z(d.getHours())}:${z(d.getMinutes())}`;
};
function BcPaceSection({ b, onChanged, flash }) {
  const sb = window.HOROS_SB;
  const [minD, setMinD] = React.useState(String(b.min_delay ?? 45));
  const [maxD, setMaxD] = React.useState(String(b.max_delay ?? 120));
  const [daily, setDaily] = React.useState(String(b.daily_limit ?? 200));
  const [wStart, setWStart] = React.useState(b.window_start ?? 8);
  const [wEnd, setWEnd] = React.useState(b.window_end ?? 20);
  const [schedAt, setSchedAt] = React.useState(_bcLocalInput(b.scheduled_at));
  const [saving, setSaving] = React.useState(false);
  const pace = (BC_PACE.find(p => String(p.min) === minD && String(p.max) === maxD) || {}).id || 'custom';
  const nMin = Number(minD), nMax = Number(maxD), nDay = Number(daily);
  const invalid = !(nMin >= 10) || !(nMax >= nMin) || !(nDay >= 1 && nDay <= 1000) || wEnd <= wStart;
  const dirty = nMin !== b.min_delay || nMax !== b.max_delay || nDay !== b.daily_limit || wStart !== b.window_start
    || wEnd !== b.window_end || (b.status === 'scheduled' && schedAt !== _bcLocalInput(b.scheduled_at));
  const pending = Math.max(0, (b.total || 0) - (b.sent || 0) - (b.failed || 0));
  // Estimativa: média do intervalo × fila, respeitando o máximo por dia e as horas da janela
  const avg = (nMin + nMax) / 2 || 1;
  const perDay = Math.max(1, Math.min(nDay || 1, Math.floor(((wEnd - wStart) * 3600) / avg)));
  const days = pending ? Math.ceil(pending / perDay) : 0;
  const estTxt = !pending ? '' : days <= 1 ? `Fila de ${pending.toLocaleString('pt-BR')} termina em cerca de ${_bcDuration(pending * avg)}`
    : `Fila de ${pending.toLocaleString('pt-BR')} leva cerca de ${days} dias (até ${perDay.toLocaleString('pt-BR')} por dia)`;

  const save = async () => {
    if (invalid) return;
    setSaving(true);
    const { error } = await sb.rpc('update_broadcast_pace', {
      p_id: b.id, p_min_delay: nMin, p_max_delay: nMax, p_daily_limit: nDay,
      p_window_start: wStart, p_window_end: wEnd,
      p_scheduled_at: b.status === 'scheduled' && schedAt ? new Date(schedAt).toISOString() : null,
    });
    setSaving(false);
    if (error) { flash(false, 'Não foi possível salvar o ritmo: ' + error.message); return; }
    flash(true, b.status === 'running' ? 'Ritmo salvo — já vale para a próxima mensagem.' : 'Ritmo salvo.');
    onChanged();
  };

  return (
    <div className="bce-sec">
      <div className="bce-label">Ritmo e volume</div>
      <div className="bc-pace">
        {BC_PACE.map(p => (
          <button key={p.id} className={`bc-pace-opt${pace === p.id ? ' on' : ''}`} onClick={() => { setMinD(String(p.min)); setMaxD(String(p.max)); }}>
            <b>{p.label}</b><span>{p.min}–{p.max}s entre mensagens</span>
          </button>
        ))}
      </div>
      <div className="bc-grid">
        <label>Intervalo mínimo (s)<input className="bc-input" type="number" min={10} value={minD} onChange={e => setMinD(e.target.value)} /></label>
        <label>Intervalo máximo (s)<input className="bc-input" type="number" min={minD} value={maxD} onChange={e => setMaxD(e.target.value)} /></label>
        <label>Máximo por dia<input className="bc-input" type="number" min={1} max={1000} value={daily} onChange={e => setDaily(e.target.value)} /></label>
        <label>Enviar das
          <div className="bc-hours">
            <select className="bc-input" value={wStart} onChange={e => { const v = Number(e.target.value); setWStart(v); if (wEnd <= v) setWEnd(v + 1); }}>{Array.from({ length: 24 }, (_, h) => <option key={h} value={h}>{h}h</option>)}</select>
            às
            <select className="bc-input" value={wEnd} onChange={e => setWEnd(Number(e.target.value))}>{Array.from({ length: 24 }, (_, h) => h + 1).filter(h => h > wStart).map(h => <option key={h} value={h}>{h}h</option>)}</select>
          </div>
        </label>
        {b.status === 'scheduled' && (
          <label>Começar em<input className="bc-input" type="datetime-local" value={schedAt} onChange={e => setSchedAt(e.target.value)} /></label>
        )}
      </div>
      {nMin < 10 && <div className="bc-error">O intervalo mínimo é 10 segundos.</div>}
      {nMax < nMin && <div className="bc-error">O intervalo máximo não pode ser menor que o mínimo.</div>}
      {(nDay < 1 || nDay > 1000) && <div className="bc-error">O máximo por dia vai de 1 a 1.000.</div>}
      <div className="bce-row">
        <span className="bce-hint">{estTxt}{estTxt && nMin < 30 ? ' · ritmo rápido aumenta o risco de bloqueio do número' : ''}</span>
        <button className="bc-btn sm primary" disabled={!dirty || invalid || saving} onClick={save}>{saving ? 'Salvando…' : 'Salvar ritmo'}</button>
      </div>
    </div>
  );
}

// Editar disparo: ritmo/volume (sempre) + nome, mensagem e fila (pausado/agendado; quem já recebeu não muda)
function BcEditPanel({ b, onClose, onChanged }) {
  const sb = window.HOROS_SB;
  const cols = (typeof useKanbanCols === 'function' ? useKanbanCols() : []) || [];
  const [name, setName] = React.useState(b.name || '');
  const [msg, setMsg] = React.useState(b.message || '');
  const [saving, setSaving] = React.useState(false);
  const [note, setNote] = React.useState(null); // { ok, text }
  const [keep, setKeep] = React.useState('');
  const [askKeep, setAskKeep] = React.useState(false);
  const [addMode, setAddMode] = React.useState('cols');
  const [addCols, setAddCols] = React.useState([]);
  const [addList, setAddList] = React.useState('');
  const [addCount, setAddCount] = React.useState(null);
  const [adding, setAdding] = React.useState(false);
  const taRef = React.useRef(null);
  const isTpl = !!b.template;
  const running = b.status === 'running';
  const pending = Math.max(0, (b.total || 0) - (b.sent || 0) - (b.failed || 0));
  const dirty = name !== (b.name || '') || (!isTpl && msg !== (b.message || ''));

  const flash = (ok, text) => { setNote({ ok, text }); setTimeout(() => setNote(null), 5000); };

  const insertVar = (v) => {
    const ta = taRef.current;
    const s = ta ? ta.selectionStart : msg.length, e = ta ? ta.selectionEnd : msg.length;
    const next = msg.slice(0, s) + v + msg.slice(e);
    setMsg(next.slice(0, BCW_MAX));
    requestAnimationFrame(() => { if (ta) { ta.focus(); ta.selectionStart = ta.selectionEnd = s + v.length; } });
  };

  const saveMsg = async () => {
    if (!isTpl && !msg.trim() && !b.media) { flash(false, 'Escreva a mensagem.'); return; }
    setSaving(true);
    const { error } = await sb.rpc('update_broadcast', { p_id: b.id, p_name: name, p_message: msg });
    setSaving(false);
    if (error) { flash(false, 'Não foi possível salvar: ' + error.message); return; }
    flash(true, 'Mensagem salva — quem ainda está na fila vai receber a nova versão.');
    onChanged();
  };

  const audience = () => addMode === 'cols'
    ? { source: 'leads', cols: addCols }
    : { source: 'list', rows: _bcParseList(addList) };

  React.useEffect(() => {
    const a = audience();
    if ((a.source === 'leads' && !addCols.length) || (a.source === 'list' && !a.rows.length)) { setAddCount(null); return; }
    let alive = true;
    const t = setTimeout(async () => {
      const { data } = await sb.rpc('broadcast_audience_count', { p_audience: a });
      if (alive) setAddCount(typeof data === 'number' ? data : null);
    }, 350);
    return () => { alive = false; clearTimeout(t); };
  }, [addMode, addCols.join(','), addList]);

  const doAdd = async () => {
    setAdding(true);
    const { data, error } = await sb.rpc('broadcast_add_recipients', { p_id: b.id, p_audience: audience() });
    setAdding(false);
    if (error) { flash(false, 'Não foi possível adicionar: ' + error.message); return; }
    const n = data || 0;
    flash(true, n ? `${n.toLocaleString('pt-BR')} ${n === 1 ? 'pessoa entrou' : 'pessoas entraram'} na fila.` : 'Todos já estavam no disparo — ninguém novo.');
    setAddCols([]); setAddList('');
    onChanged();
  };

  const keepN = parseInt(keep, 10);
  const keepOk = Number.isFinite(keepN) && keepN >= 0 && keepN < pending;
  const doKeep = async () => {
    setAskKeep(false);
    const { data, error } = await sb.rpc('broadcast_trim_queue', { p_id: b.id, p_keep: keepN });
    if (error) { flash(false, 'Não foi possível ajustar: ' + error.message); return; }
    flash(true, `${(data || 0).toLocaleString('pt-BR')} ${data === 1 ? 'pessoa saiu' : 'pessoas saíram'} da fila.`);
    setKeep('');
    onChanged();
  };

  return (
    <div className="bce">
      <div className="bce-head">
        <div>
          <div className="bce-title">Editar disparo</div>
          <div className="bce-sub">As mudanças valem para quem ainda está na fila. Quem já recebeu não muda.</div>
        </div>
        <button className="bc-btn sm" onClick={onClose}>Concluir</button>
      </div>
      {note && <div className={`bce-note${note.ok ? '' : ' err'}`}>{note.text}</div>}

      <BcPaceSection key={[b.id, b.min_delay, b.max_delay, b.daily_limit, b.window_start, b.window_end, b.scheduled_at].join(':')}
        b={b} onChanged={onChanged} flash={flash} />

      {running ? (
        <div className="bce-sec"><div className="bce-hint">Para mudar a mensagem ou quem está na fila, pause o disparo.</div></div>
      ) : (<>
      <div className="bce-sec">
        <div className="bce-label">Mensagem</div>
        <input className="bce-input" value={name} maxLength={120} onChange={e => setName(e.target.value)} placeholder="Nome do disparo" />
        {isTpl ? (
          <div className="bce-tpl">Este disparo usa um <b>modelo aprovado do WhatsApp</b> — o texto do modelo não pode ser mudado. Para outra mensagem, crie um novo disparo.
            <div className="bc-bubble-wrap"><div className="bc-bubble">{b.message}</div></div>
          </div>
        ) : (
          <>
            <textarea ref={taRef} className="bce-ta" rows={5} value={msg} maxLength={BCW_MAX} onChange={e => setMsg(e.target.value)} placeholder="Escreva a mensagem…" />
            <div className="bce-row">
              <button className="bc-btn sm" onClick={() => insertVar('{primeiro_nome}')}>+ primeiro nome</button>
              <button className="bc-btn sm" onClick={() => insertVar('{nome}')}>+ nome completo</button>
              <span className="bce-count">{msg.length}/{BCW_MAX}</span>
            </div>
            {msg.trim() && <div className="bce-prev"><span className="bce-label">Prévia</span><div className="bc-bubble-wrap"><div className="bc-bubble">{_bcPersonalize(msg, 'Maria Souza')}</div></div></div>}
          </>
        )}
        <div className="bce-row end">
          <button className="bc-btn sm primary" disabled={!dirty || saving} onClick={saveMsg}>{saving ? 'Salvando…' : 'Salvar mensagem'}</button>
        </div>
      </div>

      <div className="bce-sec">
        <div className="bce-label">Quem recebe</div>
        <div className="bce-stats">
          <span><b>{pending.toLocaleString('pt-BR')}</b> na fila</span>
          <span><b>{(b.sent || 0).toLocaleString('pt-BR')}</b> já receberam</span>
          {b.failed > 0 && <span><b>{b.failed.toLocaleString('pt-BR')}</b> falharam</span>}
        </div>
        <div className="bce-hint">Para tirar uma pessoa, use o × na lista abaixo (filtro "Na fila").</div>

        <div className="bce-sub2">Diminuir a fila</div>
        {askKeep ? (
          <div className="bce-row">
            <span className="bce-ask">Tirar {(pending - keepN).toLocaleString('pt-BR')} {pending - keepN === 1 ? 'pessoa' : 'pessoas'} da fila e manter só {keepN.toLocaleString('pt-BR')}?</span>
            <button className="bc-btn sm" onClick={() => setAskKeep(false)}>Voltar</button>
            <button className="bc-btn sm danger" onClick={doKeep}>Tirar da fila</button>
          </div>
        ) : (
          <div className="bce-row">
            <span>Manter só os primeiros</span>
            <input className="bce-input num" inputMode="numeric" value={keep} onChange={e => setKeep(e.target.value.replace(/\D/g, ''))} placeholder={String(pending)} />
            <span>da fila</span>
            <button className="bc-btn sm" disabled={!keepOk} onClick={() => setAskKeep(true)}>Aplicar</button>
          </div>
        )}

        <div className="bce-sub2">Adicionar pessoas</div>
        <div className="bc-seg">
          <button className={addMode === 'cols' ? 'on' : ''} onClick={() => setAddMode('cols')}>Etapas do funil</button>
          <button className={addMode === 'list' ? 'on' : ''} onClick={() => setAddMode('list')}>Colar lista</button>
        </div>
        {addMode === 'cols' ? (
          <div className="bce-chips">
            {cols.map(c => {
              const on = addCols.includes(c.id);
              return (
                <button key={c.id} className={`bce-chip${on ? ' on' : ''}`} onClick={() => setAddCols(on ? addCols.filter(x => x !== c.id) : [...addCols, c.id])}>
                  <span className="bce-dot" style={{ background: c.color }} />{c.title}
                </button>
              );
            })}
          </div>
        ) : (
          <textarea className="bce-ta" rows={4} value={addList} onChange={e => setAddList(e.target.value)} placeholder={'Um por linha: telefone, nome\n11999998888, Maria Souza'} />
        )}
        <div className="bce-row">
          <span className="bce-hint">{addCount == null ? 'Quem já está no disparo não entra de novo.' : `${addCount.toLocaleString('pt-BR')} ${addCount === 1 ? 'contato encontrado' : 'contatos encontrados'} · quem já está no disparo não entra de novo`}</span>
          <button className="bc-btn sm primary" disabled={!addCount || adding} onClick={doAdd}>{adding ? 'Adicionando…' : 'Adicionar à fila'}</button>
        </div>
      </div>
      </>)}
    </div>
  );
}

// ── Novo disparo: assistente em 4 passos com passos na lateral (layout pedido pelo usuário em 2026-10-03,
// a partir da imagem "Disparo em Massa no Horos", no design system do Horos) ─────────────────────────────
const BCW_STEPS = [
  { title: 'Público',        sub: 'Selecione os contatos' },
  { title: 'Mensagem',       sub: 'Personalize sua mensagem' },
  { title: 'Configurações',  sub: 'Defina o envio' },
  { title: 'Revisão',        sub: 'Confira antes de enviar' },
];
const BCW_SOURCES = [
  { id: 'contacts', icon: 'team',   title: 'Contatos',          sub: 'Escolha pessoa por pessoa' },
  { id: 'segments', icon: 'tag',    title: 'Segmentos',         sub: 'Ex.: etapas do funil, etiquetas' },
  { id: 'filters',  icon: 'funnel', title: 'Filtros avançados', sub: 'Ex.: região, temperatura, origem' },
];
const BCW_ORIGENS = [['importacao', 'Importação'], ['meta_ads', 'Meta Ads'], ['formulario', 'Formulário'], ['campanha', 'Campanha'], ['lista', 'Lista'], ['conhecido', 'Conhecido'], ['feirao', 'Feirão'], ['manual', 'Manual']];
const BCW_TEMPS = [['hot', 'Quente', 'flame'], ['warm', 'Morno', 'drop'], ['cold', 'Frio', 'snow']];
const _bcwDigits = (p) => { let d = String(p || '').replace(/\D/g, ''); if (d.length > 11 && d.startsWith('55')) d = d.slice(2); return d; };

// Blocos "+ Adicionar": cada item escolhido vira um bloco e sempre sobra um bloco para adicionar o próximo;
// clicar abre a lista (com busca) só com o que ainda não foi escolhido. Pedido do usuário 2026-10-03.
// Texto do WhatsApp: *negrito*, _itálico_, ~riscado~ → elementos (prévia)
function _bcwFormat(text) {
  const out = [];
  const re = /(\*[^*\n]+\*|_[^_\n]+_|~[^~\n]+~)/g;
  String(text || '').split('\n').forEach((line, li) => {
    if (li) out.push(<br key={'br' + li} />);
    let last = 0, m, k = 0;
    while ((m = re.exec(line))) {
      if (m.index > last) out.push(line.slice(last, m.index));
      const t = m[0], inner = t.slice(1, -1);
      out.push(t[0] === '*' ? <b key={li + '-' + k++}>{inner}</b> : t[0] === '_' ? <i key={li + '-' + k++}>{inner}</i> : <s key={li + '-' + k++}>{inner}</s>);
      last = m.index + t.length;
    }
    if (last < line.length) out.push(line.slice(last));
  });
  return out;
}
const BCW_EMOJIS = ['👋', '😊', '🙂', '😉', '😍', '🙏', '👍', '👏', '🎉', '✨', '🔥', '⭐', '✅', '📌', '📍', '📅', '⏰', '💬', '📞', '📲', '🏠', '🏡', '🏢', '🔑', '💰', '💵', '📈', '🎯', '🤝', '❤️'];
const BCW_MAX = 1000;

// Celular da prévia (cortado embaixo) — passo Mensagem e Revisão
function BcwPhone({ senderName, time, file, fileIcon, message, sampleName }) {
  return (
    <div className="bcw-phone">
      <div className="bcw-phone-status"><span>{time}</span><span className="bcw-phone-sig"><i /><i /><i /><i /></span></div>
      <div className="bcw-phone-head">
        <Icon name="arrLeft" size={16} stroke={2} />
        <span className="bcw-phone-av">{(senderName || 'H').trim()[0].toUpperCase()}</span>
        <span className="bcw-phone-who"><b>{senderName}</b><small>online</small></span>
      </div>
      <div className="bcw-phone-chat">
        <div className="bcw-bubble">
          {file && <div className="bcw-bubble-file"><Icon name={fileIcon} size={13} /> {file.name}</div>}
          {message.trim() ? _bcwFormat(_bcPersonalize(message, sampleName)) : <span className="bcw-bubble-ph">{file ? '(só o anexo)' : 'Sua mensagem aparece aqui'}</span>}
          <span className="bcw-bubble-meta">{time} <Icon name="check" size={12} stroke={2.4} /></span>
        </div>
      </div>
    </div>
  );
}

function BcwAddPicker({ options, value, onChange, addLabel, emptyText, icon }) {
  const [open, setOpen] = React.useState(false);
  const [q, setQ] = React.useState('');
  const [hi, setHi] = React.useState(0);
  const wrapRef = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const close = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) { setOpen(false); setQ(''); } };
    document.addEventListener('mousedown', close);
    return () => document.removeEventListener('mousedown', close);
  }, [open]);
  const chosen = value.map(id => options.find(o => o.id === id)).filter(Boolean);
  const norm = (t) => String(t || '').normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
  const rest = options.filter(o => !value.includes(o.id) && (!q.trim() || norm(o.label).includes(norm(q.trim()))));
  React.useEffect(() => { setHi(0); }, [q, open]);
  const add = (id) => { onChange([...value, id]); setQ(''); setOpen(false); };
  const onKey = (e) => {
    if (e.key === 'ArrowDown') { e.preventDefault(); setHi(h => Math.min(rest.length - 1, h + 1)); }
    else if (e.key === 'ArrowUp') { e.preventDefault(); setHi(h => Math.max(0, h - 1)); }
    else if (e.key === 'Enter' && rest[hi]) { e.preventDefault(); add(rest[hi].id); }
    else if (e.key === 'Escape') { e.stopPropagation(); setOpen(false); setQ(''); }
  };
  if (!options.length) return <span className="bc-muted">{emptyText}</span>;
  return (
    <div className="bcw-blocks">
      {chosen.map(o => (
        <span key={o.id} className="bcw-block">
          <span className="bc-pick-dot" style={{ background: o.color || 'var(--fg-4)' }} />
          <span className="bcw-block-txt">{o.label}</span>
          <button onClick={() => onChange(value.filter(v => v !== o.id))} aria-label={`Remover ${o.label}`} title="Remover"><Icon name="close" size={11} stroke={2.4} /></button>
        </span>
      ))}
      {rest.length > 0 || q ? (
        <div className="bcw-addwrap" ref={wrapRef}>
          <button className={`bcw-block add${open ? ' open' : ''}`} onClick={() => setOpen(o => !o)} aria-expanded={open}>
            <Icon name="plus" size={13} stroke={2.4} /> {chosen.length ? addLabel.more : addLabel.first}
          </button>
          {open && (
            <div className="bcw-pop" role="listbox">
              <div className="bcw-pop-search">
                <Icon name="search" size={13} />
                <input autoFocus value={q} onChange={e => setQ(e.target.value)} onKeyDown={onKey} placeholder="Buscar…" />
              </div>
              <div className="bcw-pop-list">
                {rest.length === 0 && <div className="bcw-empty">Nada encontrado.</div>}
                {rest.map((o, i) => (
                  <button key={o.id} role="option" aria-selected={i === hi} className={`bcw-pop-item${i === hi ? ' hl' : ''}`}
                    onMouseEnter={() => setHi(i)} onClick={() => add(o.id)}>
                    <span className="bc-pick-dot" style={{ background: o.color || 'var(--fg-4)' }} />
                    <span>{o.label}</span>
                    {o.hint != null && <small>{o.hint}</small>}
                  </button>
                ))}
              </div>
            </div>
          )}
        </div>
      ) : null}
    </div>
  );
}

// WhatsApp oficial (API do Meta): para quem não escreveu nas últimas 24 h, só sai MODELO aprovado.
// Cada variável {{1}}, {{2}}… do modelo pode usar {primeiro_nome} / {nome} — trocado contato a contato.
const _bcTplVars = body => { let m = 0; String(body || '').replace(/\{\{\s*(\d+)\s*\}\}/g, (_, n) => { m = Math.max(m, Number(n)); return _; }); return m; };
const _bcTplFill = (body, params) => String(body || '').replace(/\{\{\s*(\d+)\s*\}\}/g, (m, n) => params[Number(n) - 1] || m);

function BcwTemplatePanel({ tpl, setTpl, params, setParams }) {
  const [list, setList] = React.useState(null);
  const [err, setErr] = React.useState('');
  const [manage, setManage] = React.useState('');
  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 || []).filter(t => !(t.header && t.header.format && t.header.format !== 'TEXT')));
        setManage(d.manageUrl || '');
      } catch (e) { setErr(e.message); setList([]); }
    })();
  }, []);
  const pick = (t) => {
    setTpl(t);
    const n = _bcTplVars(t.body);
    setParams(Array.from({ length: n }, (_, i) => (i === 0 ? '{primeiro_nome}' : '')));
  };
  const cat = c => ({ MARKETING: 'Marketing', UTILITY: 'Utilidade', AUTHENTICATION: 'Autenticação' })[c] || c;
  return (
    <div className="bcw-tpl">
      {list === null && <div className="bcw-tpl-empty">Carregando modelos aprovados…</div>}
      {err && <div className="bc-error">{err}</div>}
      {list !== null && !list.length && !err && (
        <div className="bcw-tpl-empty">Nenhum modelo aprovado ainda. Crie um no WhatsApp Manager — o Meta costuma aprovar em minutos.
          {manage && <a href={manage} target="_blank" rel="noopener noreferrer">Criar modelo</a>}</div>
      )}
      {(list || []).length > 0 && (
        <div className="bcw-tpl-list">
          {list.map(t => (
            <button key={t.name + t.language} className={'bcw-tpl-item' + (tpl && tpl.name === t.name && tpl.language === t.language ? ' on' : '')} onClick={() => pick(t)}>
              <div className="bcw-tpl-top"><b>{t.name}</b><span>{cat(t.category)} · {t.language}</span></div>
              <div className="bcw-tpl-body">{t.body}</div>
            </button>
          ))}
        </div>
      )}
      {tpl && params.length > 0 && (
        <div className="bcw-tpl-params">
          {params.map((v, i) => (
            <label key={i}><span>Variável {`{{${i + 1}}}`}</span>
              <input className="bc-input" value={v} onChange={e => setParams(prev => prev.map((x, j) => j === i ? e.target.value : x))} placeholder="Texto ou {primeiro_nome}" />
              <div className="bcw-tpl-chips">
                <button type="button" onClick={() => setParams(prev => prev.map((x, j) => j === i ? '{primeiro_nome}' : x))}>{'{primeiro_nome}'}</button>
                <button type="button" onClick={() => setParams(prev => prev.map((x, j) => j === i ? '{nome}' : x))}>{'{nome}'}</button>
              </div>
            </label>
          ))}
        </div>
      )}
      {manage && (list || []).length > 0 && <a className="bcw-link" href={manage} target="_blank" rel="noopener noreferrer">Criar ou editar modelos no WhatsApp Manager</a>}
    </div>
  );
}

function BcWizard({ onDone, onCancel }) {
  const sb = window.HOROS_SB;
  const cols = window.useKanbanCols ? window.useKanbanCols() : [];
  const [step, setStep] = React.useState(1);
  // público
  const [source, setSource] = React.useState('contacts');
  const [picked, setPicked] = React.useState([]);            // [{ phone, name }]
  const [q, setQ] = React.useState('');
  const [results, setResults] = React.useState([]);
  const [searching, setSearching] = React.useState(false);
  const [showAll, setShowAll] = React.useState(false);
  const [pasteOpen, setPasteOpen] = React.useState(false);
  const sheetRef = React.useRef(null);
  const onSheet = async (e) => {
    const f = e.target.files && e.target.files[0];
    e.target.value = '';
    if (!f) return;
    try {
      const rows = await _bcReadSheet(f);
      if (!rows.length) { window.alert('Não achei telefones nessa planilha. Use uma coluna com o número (com DDD).'); return; }
      addPick(rows);
      window.dispatchEvent(new CustomEvent('horos:toast', { detail: { title: 'Planilha importada', body: `${rows.length} números lidos de ${f.name}` } }));
    } catch (err) { window.alert(err.message || 'Não foi possível ler a planilha.'); }
  };
  const [listRaw, setListRaw] = React.useState('');
  const [segMode, setSegMode] = React.useState('cols');      // cols | labels | inbox
  const [inboxLists, setInboxLists] = React.useState([]);   // planilhas do Inbox do disparo
  const [selInbox, setSelInbox] = React.useState([]);
  React.useEffect(() => { sb.rpc('broadcast_contact_lists').then(({ data }) => setInboxLists((data || []).filter(l => l.inbox > 0)), () => {}); }, []);
  const [selCols, setSelCols] = React.useState([]);
  const [labels, setLabels] = React.useState([]);
  const [selLabels, setSelLabels] = React.useState([]);
  const [fCols, setFCols] = React.useState([]);
  const [fTemps, setFTemps] = React.useState([]);
  const [fRegions, setFRegions] = React.useState([]);        // chaves normalizadas (lead_region_key)
  const [regionOpts, setRegionOpts] = React.useState(null);
  const [fOrigens, setFOrigens] = React.useState([]);
  const [count, setCount] = React.useState(null);
  const [sample, setSample] = React.useState([]);
  const [counting, setCounting] = React.useState(false);
  // mensagem
  const [name, setName] = React.useState('');
  const [message, setMessage] = React.useState('');
  // WhatsApp oficial: modelo aprovado (padrão) ou mensagem livre (só quem escreveu nas últimas 24 h)
  const [cloud, setCloud] = React.useState(false);
  const [msgMode, setMsgMode] = React.useState('template');
  const [tpl, setTpl] = React.useState(null);
  const [tplParams, setTplParams] = React.useState([]);
  React.useEffect(() => {
    const uid = (window.horosActUid && window.horosActUid()) || window.HOROS_USER?.id;
    if (!uid || !sb) return;
    sb.from('wa_instances').select('provider').eq('user_id', uid).maybeSingle()
      .then(({ data }) => setCloud(data?.provider === 'cloud')).catch(() => {});
  }, []);
  const useTpl = cloud && msgMode === 'template';
  const [file, setFile] = React.useState(null);
  const taRef = React.useRef(null);
  const fileRef = React.useRef(null);
  // configurações
  const [pace, setPace] = React.useState('safe');
  const [minD, setMinD] = React.useState(45);
  const [maxD, setMaxD] = React.useState(120);
  const [daily, setDaily] = React.useState(200);
  const [wStart, setWStart] = React.useState(8);
  const [wEnd, setWEnd] = React.useState(20);
  const [when, setWhen] = React.useState('now');
  const [schedAt, setSchedAt] = React.useState('');
  // depois do envio: etapa do funil para onde o lead vai ('' = não mexe)
  const [afterCol, setAfterCol] = React.useState('');
  const [saving, setSaving] = React.useState(false);
  const [err, setErr] = React.useState('');

  React.useEffect(() => {
    const uid = window.horosActUid?.() || window.HOROS_USER?.id;
    sb.from('wa_labels').select('label_id,name,color').eq('user_id', uid).then(({ data }) => {
      setLabels((data || []).filter(l => l.name && !/^(n[aã]o lidas|favoritos|grupos)$/i.test(l.name)));
    });
  }, []);

  // Regiões que existem nos leads (mesma região com/sem acento ou maiúscula = uma opção só)
  React.useEffect(() => {
    if (source !== 'filters' || regionOpts !== null) return;
    sb.rpc('broadcast_region_options').then(({ data }) => {
      setRegionOpts((data || []).map(r => ({ id: r.key, label: r.label, hint: `${r.n} ${r.n === 1 ? 'lead' : 'leads'}` })));
    }, () => setRegionOpts([]));
  }, [source]);

  // Busca de contatos: leads (RLS) + conversas do WhatsApp do usuário
  React.useEffect(() => {
    const term = q.trim().replace(/[,()*%]/g, ' ').trim();
    if (term.length < 2) { setResults([]); setSearching(false); return; }
    let alive = true;
    setSearching(true);
    const t = setTimeout(async () => {
      const dig = term.replace(/\D/g, '');
      const leadOr = dig.length >= 4 ? `name.ilike.*${term}*,phone.ilike.*${dig}*` : `name.ilike.*${term}*`;
      const convOr = dig.length >= 4 ? `contact_name.ilike.*${term}*,contact_phone.ilike.*${dig}*` : `contact_name.ilike.*${term}*`;
      const [{ data: ls }, { data: cs }] = await Promise.all([
        sb.from('leads').select('id,name,phone').eq('archived', false).or(leadOr).limit(12),
        sb.from('conversations').select('id,contact_name,contact_phone,wa_jid').eq('user_id', window.horosActUid?.() || window.HOROS_USER?.id).like('wa_jid', '%@s.whatsapp.net').or(convOr).limit(12),
      ]);
      if (!alive) return;
      const seen = new Set(), out = [];
      (ls || []).forEach(l => { const k = _bcwDigits(l.phone); if (k.length >= 10 && !seen.has(k)) { seen.add(k); out.push({ phone: k, name: l.name || '', kind: 'Lead' }); } });
      (cs || []).forEach(c => { const k = _bcwDigits(c.contact_phone || String(c.wa_jid).split('@')[0]); if (k.length >= 10 && !seen.has(k)) { seen.add(k); out.push({ phone: k, name: c.contact_name || '', kind: 'WhatsApp' }); } });
      setResults(out.slice(0, 12));
      setSearching(false);
    }, 280);
    return () => { alive = false; clearTimeout(t); };
  }, [q]);

  const pickedKeys = React.useMemo(() => new Set(picked.map(p => _bcwDigits(p.phone))), [picked]);
  const addPick = (rows) => setPicked(prev => {
    const keys = new Set(prev.map(p => _bcwDigits(p.phone)));
    const next = [...prev];
    rows.forEach(r => { const k = _bcwDigits(r.phone); if (k.length >= 10 && !keys.has(k)) { keys.add(k); next.push({ phone: k, name: r.name || '' }); } });
    return next;
  });
  const removePick = (phone) => setPicked(prev => prev.filter(p => _bcwDigits(p.phone) !== _bcwDigits(phone)));

  const audience = React.useMemo(() => {
    if (source === 'contacts') return { source: 'list', rows: picked };
    if (source === 'segments') return segMode === 'inbox' ? { source: 'inbox', lists: selInbox } : segMode === 'labels' ? { source: 'labels', labels: selLabels } : { source: 'leads', cols: selCols };
    return { source: 'leads', cols: fCols, temps: fTemps, regions: fRegions, origens: fOrigens };
  }, [source, picked, segMode, selInbox, selCols, selLabels, fCols, fTemps, fRegions, fOrigens]);

  // Quantos recebem (sem repetidos e sem número inválido) + amostra para a prévia
  React.useEffect(() => {
    const empty = (source === 'contacts' && picked.length === 0)
      || (source === 'segments' && segMode === 'labels' && selLabels.length === 0)
      || (source === 'segments' && segMode === 'cols' && selCols.length === 0);
    if (empty) { setCount(0); setSample([]); setCounting(false); return; }
    let alive = true;
    setCounting(true);
    const t = setTimeout(async () => {
      const [{ data: c }, { data: s }] = await Promise.all([
        sb.rpc('broadcast_audience_count', { p_audience: audience }),
        sb.rpc('broadcast_audience', { p_audience: audience }).limit(5),
      ]);
      if (!alive) return;
      setCount(c ?? 0); setSample(s || []); setCounting(false);
    }, 350);
    return () => { alive = false; clearTimeout(t); };
  }, [JSON.stringify(audience)]);

  const toggle = (arr, set, v) => set(arr.includes(v) ? arr.filter(x => x !== v) : [...arr, v]);
  const colOpts = React.useMemo(() => cols.map(c => ({ id: c.id, label: c.title, color: c.color })), [cols]);
  const labelOpts = React.useMemo(() => labels.map(l => ({ id: l.label_id, label: l.name, color: _BC_WA_COLORS[l.color] || '#94a3b8' })), [labels]);
  const insertVar = (v) => {
    const el = taRef.current;
    if (!el) { setMessage(m => m + v); return; }
    const s = el.selectionStart, e = el.selectionEnd;
    setMessage(message.slice(0, s) + v + message.slice(e));
    requestAnimationFrame(() => { el.focus(); el.selectionStart = el.selectionEnd = s + v.length; });
  };
  const pickPace = (p) => { setPace(p.id); setMinD(p.min); setMaxD(p.max); };
  const [emojiOpen, setEmojiOpen] = React.useState(false);
  const [varsOpen, setVarsOpen] = React.useState(false);
  // Envolve a seleção (ou insere o par) com o marcador do WhatsApp
  const wrapSel = (mk) => {
    const el = taRef.current;
    const s0 = el ? el.selectionStart : message.length, e0 = el ? el.selectionEnd : message.length;
    const sel = message.slice(s0, e0);
    const next = (message.slice(0, s0) + mk + sel + mk + message.slice(e0)).slice(0, BCW_MAX);
    setMessage(next);
    requestAnimationFrame(() => { if (!el) return; el.focus(); el.selectionStart = s0 + mk.length; el.selectionEnd = s0 + mk.length + sel.length; });
  };
  const insertText = (v) => { setEmojiOpen(false); setVarsOpen(false); if (message.length + v.length > BCW_MAX) return; insertVar(v); };
  const senderName = (window.HOROS_WORKSPACE && (window.HOROS_WORKSPACE.workspace_name || '')) || 'Você';
  const nowHM = new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });

  const avg = (Number(minD) + Number(maxD)) / 2;
  const perDay = Math.min(Number(daily) || 1, Math.floor(((wEnd - wStart) * 3600) / Math.max(avg, 1)));
  const days = count ? Math.ceil(count / Math.max(perDay, 1)) : 0;
  const estimate = count ? (days <= 1 ? _bcDuration(count * avg) : `${days} dias (até ${perDay}/dia)`) : '—';

  const canNext1 = (count || 0) > 0 && !counting;
  const canNext2 = useTpl ? (!!tpl && tplParams.every(v => String(v).trim())) : (message.trim().length > 0 || !!file);
  // Prévia: no modelo, o texto do modelo com as variáveis (o BcwPhone troca {primeiro_nome} pelo exemplo)
  const shownMsg = useTpl ? (tpl ? _bcTplFill(tpl.body, tplParams) : '') : message;
  const shownFile = useTpl ? null : file;
  const canNext3 = Number(minD) >= 10 && Number(maxD) >= Number(minD) && Number(daily) >= 1 && (when === 'now' || !!schedAt);
  const can = [canNext1, canNext2, canNext3, true];
  const go = (n) => { if (n < step || can.slice(0, n - 1).every(Boolean)) setStep(n); };

  const submit = async () => {
    setErr(''); setSaving(true);
    try {
      let media = null;
      if (file && !useTpl) {
        const uid = window.HOROS_USER?.id;
        const safe = file.name.replace(/[^\w.\-]+/g, '_').slice(-80);
        const path = `${uid}/${crypto.randomUUID()}/${safe}`;
        const { error: upErr } = await sb.storage.from('broadcast-media').upload(path, file, { contentType: file.type || 'application/octet-stream' });
        if (upErr) throw new Error('Falha ao enviar o arquivo: ' + upErr.message);
        const mediaType = file.type.startsWith('image/') ? 'image' : file.type.startsWith('video/') ? 'video' : file.type.startsWith('audio/') ? 'audio' : 'document';
        media = { path, mediaType, mimetype: file.type || 'application/octet-stream', fileName: file.name };
      }
      const scheduled = when === 'later' && schedAt ? new Date(schedAt).toISOString() : null;
      const { error } = await sb.rpc('create_broadcast', {
        p_name: name || `Disparo ${new Date().toLocaleDateString('pt-BR')}`,
        p_message: useTpl ? '' : message, p_media: useTpl ? null : media, p_audience: audience,
        p_min_delay: Number(minD), p_max_delay: Number(maxD), p_daily_limit: Number(daily),
        p_window_start: Number(wStart), p_window_end: Number(wEnd), p_scheduled_at: scheduled,
        p_after_col: afterCol || null,
        ...(useTpl ? { p_template: { name: tpl.name, language: tpl.language, params: tplParams, body: tpl.body } } : {}),
      });
      if (error) throw new Error(error.message);
      onDone();
    } catch (e) {
      setErr(e.message || 'Erro ao criar o disparo');
      setSaving(false);
    }
  };

  const afterColMeta = afterCol ? cols.find(c => c.id === afterCol) : null;
  // Público de uma etapa só → sugere a etapa seguinte do funil (ex.: 2º contato → 3º contato)
  const afterSuggest = React.useMemo(() => {
    const one = source === 'segments' && segMode === 'cols' ? selCols : source === 'filters' ? fCols : [];
    if (one.length !== 1) return null;
    const i = cols.findIndex(c => c.id === one[0]);
    if (i < 0 || i >= cols.length - 1) return null;
    return { id: cols[i + 1].id, title: cols[i + 1].title, from: cols[i].title };
  }, [source, segMode, selCols, fCols, cols]);
  const sampleName = sample[0]?.name || picked[0]?.name || 'Maria Silva';
  const fileIcon = file ? (file.type.startsWith('image/') ? 'image' : file.type.startsWith('audio/') ? 'mic' : 'file') : 'file';
  const srcMeta = BCW_SOURCES.find(s => s.id === source);
  const audienceText = source === 'contacts' ? `${picked.length} ${picked.length === 1 ? 'contato escolhido' : 'contatos escolhidos'}`
    : source === 'segments'
      ? (segMode === 'inbox' ? (selInbox.length ? `Planilhas: ${selInbox.join(', ')}` : 'Todo o Inbox de contatos importados')
        : segMode === 'labels'
          ? `Etiquetas: ${labels.filter(l => selLabels.includes(l.label_id)).map(l => l.name).join(', ')}`
          : `Etapas: ${cols.filter(c => selCols.includes(c.id)).map(c => c.title).join(', ')}`)
      : [fCols.length ? `Etapas: ${cols.filter(c => fCols.includes(c.id)).map(c => c.title).join(', ')}` : 'Todas as etapas',
         fTemps.length ? BCW_TEMPS.filter(t => fTemps.includes(t[0])).map(t => t[1]).join(', ') : '',
         fRegions.length ? `Região: ${fRegions.map(k => (regionOpts || []).find(o => o.id === k)?.label || k).join(', ')}` : '',
         fOrigens.length ? `Origem: ${BCW_ORIGENS.filter(o => fOrigens.includes(o[0])).map(o => o[1]).join(', ')}` : ''].filter(Boolean).join(' · ');
  const CHIP_MAX = 10;
  const chips = showAll ? picked : picked.slice(0, CHIP_MAX);

  return (
    <div className="bcw">
      <aside className="bcw-steps" aria-label="Passos">
        {BCW_STEPS.map((s, i) => {
          const n = i + 1, on = step === n, done = step > n;
          return (
            <button key={s.title} className={`bcw-step${on ? ' on' : ''}${done ? ' done' : ''}`} onClick={() => go(n)} aria-current={on ? 'step' : undefined}>
              <span className="bcw-step-n">{done ? <Icon name="check" size={12} stroke={3} /> : n}</span>
              <span className="bcw-step-txt"><b>{s.title}</b><small>{s.sub}</small></span>
            </button>
          );
        })}
        {step > 1 && (
          <div className="bcw-aud">
            <div className="bcw-aud-top">
              <span className="bcw-aud-ico"><Icon name="team" size={18} stroke={1.8} /></span>
              <div><small>Contatos selecionados</small><b>{(count || 0).toLocaleString('pt-BR')} <Icon name="check" size={13} stroke={2.6} /></b></div>
            </div>
            <div className="bcw-aud-row"><span>Válidos para envio</span><b>{(count || 0).toLocaleString('pt-BR')}</b></div>
            {source === 'contacts' && picked.length > (count || 0) && (
              <div className="bcw-aud-row warn"><span>Repetidos ou inválidos</span><b>{picked.length - (count || 0)}</b></div>
            )}
            <p className="bcw-aud-note">Quem não tiver WhatsApp é pulado na hora do envio.</p>
          </div>
        )}
      </aside>

      <section className="bcw-main">
        {step === 1 && (
          <>
            <div className="bcw-h"><h2>Público</h2><p>Escolha para quem você deseja enviar a mensagem.</p></div>
            <div className="bcw-sources">
              {BCW_SOURCES.map(s => (
                <button key={s.id} className={`bcw-src${source === s.id ? ' on' : ''}`} onClick={() => setSource(s.id)} aria-pressed={source === s.id}>
                  <span className="bcw-src-ico"><Icon name={s.icon} size={20} stroke={1.7} /></span>
                  <span className="bcw-src-txt"><b>{s.title}</b><small>{s.sub}</small></span>
                  {source === s.id && count > 0 && <span className="bcw-src-n">{count.toLocaleString('pt-BR')}</span>}
                </button>
              ))}
            </div>

            <div className="bcw-card">
              {source === 'contacts' && (
                <>
                  <div className="bcw-card-h">Selecionar contatos</div>
                  <div className="bcw-search">
                    <Icon name="search" size={15} />
                    <input value={q} onChange={e => setQ(e.target.value)} placeholder="Buscar por nome ou número…" autoFocus />
                    {searching && <span className="bcw-spin" />}
                  </div>
                  {q.trim().length >= 2 && (
                    <div className="bcw-results">
                      {results.length === 0 && !searching && <div className="bcw-empty">Ninguém encontrado com "{q.trim()}".</div>}
                      {results.length > 1 && (
                        <button className="bcw-add-all" onClick={() => addPick(results)}>
                          <Icon name="plus" size={12} stroke={2.4} /> Adicionar todos ({results.length})
                        </button>
                      )}
                      {results.map(r => {
                        const isIn = pickedKeys.has(r.phone);
                        return (
                          <button key={r.phone} className={`bcw-res${isIn ? ' in' : ''}`} onClick={() => (isIn ? removePick(r.phone) : addPick([r]))}>
                            <span className="bcw-res-name">{r.name || 'Sem nome'}</span>
                            <span className="bcw-res-phone">{fmtPhone(r.phone)}</span>
                            <span className="bcw-res-kind">{r.kind}</span>
                            <span className="bcw-res-act"><Icon name={isIn ? 'check' : 'plus'} size={13} stroke={2.4} /></span>
                          </button>
                        );
                      })}
                    </div>
                  )}

                  <div className="bcw-sel-h">
                    <span><Icon name="team" size={16} stroke={1.8} /> <b>{picked.length.toLocaleString('pt-BR')}</b> {picked.length === 1 ? 'contato selecionado' : 'contatos selecionados'}</span>
                    <div className="bcw-sel-act">
                      <button className="bcw-link" onClick={() => sheetRef.current && sheetRef.current.click()}><Icon name="file" size={13} /> Importar planilha</button>
                      <input ref={sheetRef} type="file" accept=".xlsx,.xls,.csv" style={{ display: 'none' }} onChange={onSheet} />
                      <button className="bcw-link" onClick={() => setPasteOpen(o => !o)}><Icon name="file" size={13} /> Colar lista</button>
                      {picked.length > 0 && <button className="bcw-ghost" onClick={() => { setPicked([]); setShowAll(false); }}><Icon name="close" size={12} stroke={2.2} /> Limpar seleção</button>}
                    </div>
                  </div>
                  {pasteOpen && (
                    <div className="bcw-paste">
                      <textarea className="bc-textarea mono" rows={5} value={listRaw} onChange={e => setListRaw(e.target.value)}
                        placeholder={'Um por linha: telefone, nome (pode colar do Excel)\n(11) 99999-0000, Maria Silva\n11988887777, João'} />
                      <div className="bcw-paste-foot">
                        <span>{_bcParseList(listRaw).length} números reconhecidos</span>
                        <button className="bc-btn sm primary" disabled={!_bcParseList(listRaw).length}
                          onClick={() => { addPick(_bcParseList(listRaw)); setListRaw(''); setPasteOpen(false); }}>Adicionar à seleção</button>
                      </div>
                    </div>
                  )}
                  {picked.length > 0 && (
                    <div className="bcw-chips">
                      {chips.map(p => (
                        <span key={p.phone} className="bcw-chip" title={fmtPhone(p.phone)}>
                          <Icon name="whats" size={14} stroke={1.8} />
                          <span>{p.name || fmtPhone(p.phone)}</span>
                          <button onClick={() => removePick(p.phone)} aria-label={`Remover ${p.name || p.phone}`}><Icon name="close" size={11} stroke={2.4} /></button>
                        </span>
                      ))}
                      {picked.length > CHIP_MAX && (
                        <button className="bcw-chip more" onClick={() => setShowAll(v => !v)}>
                          <Icon name="team" size={14} stroke={1.8} /> {showAll ? 'Mostrar menos' : `+ ${(picked.length - CHIP_MAX).toLocaleString('pt-BR')} contatos`}
                        </button>
                      )}
                    </div>
                  )}
                </>
              )}

              {source === 'segments' && (
                <>
                  <div className="bcw-card-h">Escolher segmento</div>
                  <div className="bc-seg bcw-seg">
                    <button className={segMode === 'cols' ? 'on' : ''} onClick={() => setSegMode('cols')}>Etapas do funil</button>
                    <button className={segMode === 'labels' ? 'on' : ''} onClick={() => setSegMode('labels')}>Etiquetas do WhatsApp</button>
                    <button className={segMode === 'inbox' ? 'on' : ''} onClick={() => setSegMode('inbox')}>Planilhas do Inbox</button>
                  </div>
                  {segMode === 'inbox' ? <>
                      <BcwAddPicker options={inboxLists.map(l => ({ id: l.nome, label: `${l.nome} (${l.inbox})` }))} value={selInbox} onChange={setSelInbox}
                        addLabel={{ first: 'Adicionar planilha', more: 'Adicionar outra planilha' }} emptyText="Nenhuma planilha importada no Inbox." />
                      <div style={{ fontSize: 12, color: 'var(--fg-3)', marginTop: 8 }}>Sem escolher nenhuma, vai para todo o Inbox de contatos importados.</div>
                    </>
                  : segMode === 'cols'
                    ? <BcwAddPicker options={colOpts} value={selCols} onChange={setSelCols}
                        addLabel={{ first: 'Adicionar etapa', more: 'Adicionar outra etapa' }} emptyText="Nenhuma etapa no funil." />
                    : <BcwAddPicker options={labelOpts} value={selLabels} onChange={setSelLabels}
                        addLabel={{ first: 'Adicionar etiqueta', more: 'Adicionar outra etiqueta' }} emptyText="Nenhuma etiqueta encontrada no seu WhatsApp." />}
                </>
              )}

              {source === 'filters' && (
                <div className="bcw-filters">
                  <div className="bcw-card-h">Filtrar leads do CRM</div>
                  <label className="bcw-flabel">Etapas <span>(nenhuma = todas)</span></label>
                  <BcwAddPicker options={colOpts} value={fCols} onChange={setFCols}
                    addLabel={{ first: 'Adicionar etapa', more: 'Adicionar outra etapa' }} emptyText="Nenhuma etapa no funil." />
                  <div className="bcw-frow">
                    <div>
                      <label className="bcw-flabel">Temperatura</label>
                      <div className="bc-chips">
                        {BCW_TEMPS.map(([v, l, ic]) => (
                          <button key={v} className={`bc-pick${fTemps.includes(v) ? ' on' : ''}`} onClick={() => toggle(fTemps, setFTemps, v)}>
                            <Icon name={ic} size={12} stroke={2} />{l}
                          </button>
                        ))}
                      </div>
                    </div>
                  </div>
                  <label className="bcw-flabel">Região <span>(as que existem nos seus leads · maiúscula e acento não importam)</span></label>
                  {regionOpts === null
                    ? <span style={{ display: 'flex', gap: 6 }}><Skel w={90} h={26} r={8} /><Skel w={70} h={26} r={8} /><Skel w={110} h={26} r={8} /></span>
                    : <BcwAddPicker options={regionOpts} value={fRegions} onChange={setFRegions}
                        addLabel={{ first: 'Adicionar região', more: 'Adicionar outra região' }} emptyText="Nenhum lead com região preenchida." />}
                  <label className="bcw-flabel">Origem do lead</label>
                  <div className="bc-chips">
                    {BCW_ORIGENS.map(([v, l]) => (
                      <button key={v} className={`bc-pick${fOrigens.includes(v) ? ' on' : ''}`} onClick={() => toggle(fOrigens, setFOrigens, v)}>{l}</button>
                    ))}
                  </div>
                </div>
              )}

              <div className="bcw-count">
                <b>{counting ? '…' : (count ?? 0).toLocaleString('pt-BR')}</b>
                <span>{count === 1 ? 'contato vai receber' : 'contatos vão receber'} · números repetidos e inválidos já removidos</span>
                {sample.length > 0 && source !== 'contacts' && <small>{sample.map(s => s.name || fmtPhone(s.phone)).join(' · ')}{count > sample.length ? ' …' : ''}</small>}
              </div>
              <div className="bcw-info">
                <Icon name="info" size={15} stroke={1.8} />
                Você pode escolher contatos específicos, segmentos ou usar filtros avançados para encontrar o público ideal.
              </div>
            </div>
          </>
        )}

        {step === 2 && (
          <div className="bcw-msg-grid">
            <div className="bcw-panel">
              <div className="bcw-panel-h"><Icon name="msgs" size={18} stroke={1.8} /><b>Mensagem</b></div>
              <p className="bcw-panel-sub">{useTpl ? 'Pelo WhatsApp oficial, o disparo usa um modelo aprovado pelo Meta. Escolha o modelo e preencha as variáveis.' : 'Escreva sua mensagem. Você pode usar variáveis para personalizar.'}</p>
              <input className="bc-input bcw-name" value={name} onChange={e => setName(e.target.value)} placeholder="Nome do disparo (só para você) — ex.: Feirão MCMV outubro" />
              {cloud && (
                <div className="bc-seg bcw-mode">
                  <button className={msgMode === 'template' ? 'on' : ''} aria-pressed={msgMode === 'template'} onClick={() => setMsgMode('template')}>Modelo aprovado</button>
                  <button className={msgMode === 'free' ? 'on' : ''} aria-pressed={msgMode === 'free'} onClick={() => setMsgMode('free')}>Mensagem livre</button>
                </div>
              )}
              {cloud && msgMode === 'free' && <div className="bc-warn" style={{ margin: 0 }}><Icon name="alert" size={14} /><span>Mensagem livre só chega para quem escreveu para você nas <b>últimas 24 h</b>. Os demais contatos ficam como falha.</span></div>}
              {useTpl && <BcwTemplatePanel tpl={tpl} setTpl={setTpl} params={tplParams} setParams={setTplParams} />}
              <div className="bcw-editor" style={useTpl ? { display: 'none' } : undefined}>
                <textarea ref={taRef} value={message} maxLength={BCW_MAX} onChange={e => setMessage(e.target.value.slice(0, BCW_MAX))}
                  placeholder={`Olá {primeiro_nome}! \n\nTemos uma novidade sobre o ${segTerm('product', 'imóvel')} que você procurou…`} />
                <div className="bcw-count-chars">{message.length.toLocaleString('pt-BR')}/{BCW_MAX.toLocaleString('pt-BR')}</div>
                <div className="bcw-toolbar">
                  <button title="Negrito (*texto*)" onClick={() => wrapSel('*')}><b>B</b></button>
                  <button title="Itálico (_texto_)" onClick={() => wrapSel('_')}><i style={{ fontFamily: 'Georgia, serif' }}>I</i></button>
                  <button title="Inserir link" onClick={() => insertText('https://')}><Icon name="link" size={15} stroke={1.9} /></button>
                  <div className="bcw-tb-pop-wrap">
                    <button title="Emoji" onClick={() => { setEmojiOpen(o => !o); setVarsOpen(false); }} className={emojiOpen ? 'on' : ''}><Icon name="smile" size={15} stroke={1.9} /></button>
                    {emojiOpen && (
                      <div className="bcw-tb-pop emoji">
                        {BCW_EMOJIS.map(em => <button key={em} onClick={() => insertText(em)}>{em}</button>)}
                      </div>
                    )}
                  </div>
                  <button title="Anexar foto, vídeo, áudio ou documento (até 50 MB)" onClick={() => fileRef.current?.click()}><Icon name="paperclip" size={15} stroke={1.9} /></button>
                  <span className="bcw-tb-sep" />
                  <div className="bcw-tb-pop-wrap">
                    <button className={`bcw-tb-vars${varsOpen ? ' on' : ''}`} onClick={() => { setVarsOpen(o => !o); setEmojiOpen(false); }}>
                      <Icon name="user" size={14} stroke={1.9} /> Variáveis <Icon name="arrDown" size={12} stroke={2.2} />
                    </button>
                    {varsOpen && (
                      <div className="bcw-tb-pop vars">
                        <button onClick={() => insertText('{primeiro_nome}')}><code>{'{primeiro_nome}'}</code><span>Primeiro nome</span></button>
                        <button onClick={() => insertText('{nome}')}><code>{'{nome}'}</code><span>Nome completo</span></button>
                        <button onClick={() => insertText('{Oi|Olá|Tudo bem}')}><code>{'{Oi|Olá}'}</code><span>Variação — cada contato recebe uma</span></button>
                      </div>
                    )}
                  </div>
                </div>
              </div>
              <input ref={fileRef} type="file" hidden accept="image/*,video/*,audio/*,.pdf,.doc,.docx,.xls,.xlsx"
                onChange={e => { const f = e.target.files?.[0]; if (f && f.size > 50 * 1024 * 1024) { setErr('Arquivo acima de 50 MB'); return; } setErr(''); setFile(f || null); e.target.value = ''; }} />
              {file && !useTpl && (
                <div className="bc-file">
                  <Icon name={fileIcon} size={14} /><span>{file.name}</span>
                  <button onClick={() => setFile(null)} title="Remover"><Icon name="close" size={12} /></button>
                </div>
              )}
              {err && <div className="bc-error">{err}</div>}
              <div className="bcw-tip">
                <Icon name="sparkles" size={16} stroke={1.8} />
                <div><b>Dica</b><span>Use variáveis para deixar a mensagem mais pessoal, como {'{primeiro_nome}'}, e variações como {'{Oi|Olá}'} — reduz o risco de bloqueio. Evite links encurtados e textos em maiúsculas.</span></div>
              </div>
            </div>

            <div className="bcw-panel">
              <div className="bcw-panel-h"><b>Prévia</b></div>
              <p className="bcw-panel-sub">Veja como sua mensagem vai chegar no WhatsApp{sampleName ? ` de ${sampleName.split(' ')[0]}` : ''}.</p>
              <BcwPhone senderName={senderName} time={nowHM} file={shownFile} fileIcon={fileIcon} message={shownMsg} sampleName={sampleName} />
              <div className="bcw-feats">
                <div><span><Icon name="user" size={16} stroke={1.8} /></span>Personalize com variáveis</div>
                <div><span><Icon name="msgs" size={16} stroke={1.8} /></span>Mantenha seu tom de voz</div>
                <div><span><Icon name="trending" size={16} stroke={1.8} /></span>Aumente suas conversões</div>
              </div>
            </div>
          </div>
        )}

        {step === 3 && (
          <>
            <div className="bcw-h"><h2>Configurações</h2><p>Defina o ritmo e quando o envio acontece.</p></div>
            <div className="bc-label">Ritmo de envio</div>
            <div className="bc-pace">
              {BC_PACE.map(p => (
                <button key={p.id} className={`bc-pace-opt${pace === p.id ? ' on' : ''}`} onClick={() => pickPace(p)}>
                  <b>{p.label}</b><span>{p.min}–{p.max}s entre mensagens</span><small>{p.hint}</small>
                </button>
              ))}
            </div>
            <div className="bc-grid">
              <label>Intervalo mínimo (s)<input className="bc-input" type="number" min={10} value={minD} onChange={e => { setMinD(e.target.value); setPace('custom'); }} /></label>
              <label>Intervalo máximo (s)<input className="bc-input" type="number" min={minD} value={maxD} onChange={e => { setMaxD(e.target.value); setPace('custom'); }} /></label>
              <label>Máximo por dia<input className="bc-input" type="number" min={1} max={1000} value={daily} onChange={e => setDaily(e.target.value)} /></label>
              <label>Enviar das
                <div className="bc-hours">
                  <select className="bc-input" value={wStart} onChange={e => setWStart(Number(e.target.value))}>{Array.from({ length: 24 }, (_, h) => <option key={h} value={h}>{h}h</option>)}</select>
                  às
                  <select className="bc-input" value={wEnd} onChange={e => setWEnd(Number(e.target.value))}>{Array.from({ length: 24 }, (_, h) => h + 1).filter(h => h > wStart).map(h => <option key={h} value={h}>{h}h</option>)}</select>
                </div>
              </label>
            </div>
            {Number(minD) < 10 && <div className="bc-error">O intervalo mínimo é 10 segundos.</div>}
            <div className="bc-label">Início</div>
            <div className="bc-seg">
              <button className={when === 'now' ? 'on' : ''} onClick={() => setWhen('now')}>Agora</button>
              <button className={when === 'later' ? 'on' : ''} onClick={() => setWhen('later')}>Agendar</button>
            </div>
            {when === 'later' && <input className="bc-input" type="datetime-local" value={schedAt} onChange={e => setSchedAt(e.target.value)} style={{ maxWidth: 260, marginTop: 8 }} />}
            <div className="bc-label">Depois do envio</div>
            <div className="bcw-after">
              <div className="bcw-after-txt">
                <b>Mover o lead para a etapa</b>
                <small>Quando a mensagem for entregue ao WhatsApp, o lead vai sozinho para esta etapa do seu funil. Quem não recebeu (sem WhatsApp ou falha) continua onde está.</small>
              </div>
              <div className="bcw-after-pick">
                {afterColMeta && <span className="bcw-after-dot" style={{ background: afterColMeta.color }} />}
                <select className="bc-input" value={afterCol} onChange={e => setAfterCol(e.target.value)} aria-label="Etapa depois do envio">
                  <option value="">Não mover</option>
                  {cols.map(c => <option key={c.id} value={c.id}>{c.title}</option>)}
                </select>
              </div>
              {afterSuggest && !afterCol && (
                <button className="bcw-link bcw-after-sug" onClick={() => setAfterCol(afterSuggest.id)}>
                  Sugestão: depois de “{afterSuggest.from}”, mover para “{afterSuggest.title}”
                </button>
              )}
            </div>
            <div className="bc-summary">
              <div><span>Contatos</span><b>{(count || 0).toLocaleString('pt-BR')}</b></div>
              <div><span>Tempo estimado</span><b>{estimate}</b></div>
              <div><span>Horário</span><b>{wStart}h às {wEnd}h (Brasília)</b></div>
            </div>
          </>
        )}

        {step === 4 && (
          <>
            <div className="bcw-h"><h2>Revisão</h2><p>Confira tudo antes de enviar. Depois de iniciado, dá para pausar ou cancelar na lista de disparos.</p></div>
            <div className="bcw-review">
              <div className="bcw-review-col">
              <div className="bcw-rv">
                <div className="bcw-rv-h"><span>Público</span><button className="bcw-link" onClick={() => setStep(1)}>Editar</button></div>
                <div className="bcw-rv-big">{(count || 0).toLocaleString('pt-BR')} <small>{count === 1 ? 'contato' : 'contatos'}</small></div>
                <div className="bcw-rv-line"><Icon name={srcMeta.icon} size={13} stroke={1.8} /> {srcMeta.title}</div>
                <div className="bcw-rv-sub">{audienceText}</div>
              </div>
              <div className="bcw-rv">
                <div className="bcw-rv-h"><span>Configurações</span><button className="bcw-link" onClick={() => setStep(3)}>Editar</button></div>
                <div className="bcw-rv-line"><Icon name="gauge" size={13} stroke={1.8} /> {(BC_PACE.find(p => p.id === pace) || { label: 'Personalizado' }).label} · {minD}–{maxD}s entre mensagens</div>
                <div className="bcw-rv-line"><Icon name="clock" size={13} stroke={1.8} /> {wStart}h às {wEnd}h · até {daily}/dia</div>
                <div className="bcw-rv-line"><Icon name="cal" size={13} stroke={1.8} /> {when === 'later' && schedAt ? `Começa ${_bcFmtDate(schedAt)}` : 'Começa agora'}</div>
                <div className="bcw-rv-line"><Icon name="hourglass" size={13} stroke={1.8} /> Tempo estimado: {estimate}</div>
                <div className="bcw-rv-line"><Icon name="arrRight" size={13} stroke={1.8} /> {afterColMeta ? <>Depois do envio, vai para <b>&nbsp;{afterColMeta.title}</b></> : 'Depois do envio, o lead continua na mesma etapa'}</div>
              </div>
              </div>
              <div className="bcw-rv bcw-rv-msg">
                <div className="bcw-rv-h"><span>Mensagem{name ? ` · ${name}` : ''}</span><button className="bcw-link" onClick={() => setStep(2)}>Editar</button></div>
                <div className="bcw-rv-sub">Como chega no WhatsApp de {sampleName.split(' ')[0]}</div>
                <BcwPhone senderName={senderName} time={nowHM} file={shownFile} fileIcon={fileIcon} message={shownMsg} sampleName={sampleName} />
                <div className="bcw-rv-phone-line" />
              </div>
            </div>
            <div className="bc-warn">
              <Icon name="alert" size={14} />
              {cloud ? <span>Pelo <b>WhatsApp oficial</b>, cada modelo enviado é cobrado pelo Meta na sua conta do WhatsApp Business. Se muitas pessoas bloquearem ou denunciarem, o Meta reduz a qualidade do número e limita os envios. O disparo pausa sozinho se o Meta recusar.</span> :
              <span>Envios em massa pelo WhatsApp comum podem levar ao <b>bloqueio do número</b> se muitas pessoas denunciarem ou se o ritmo for alto. Prefira contatos que conhecem você e o ritmo <b>Seguro</b>. O disparo pausa sozinho se o WhatsApp desconectar ou houver falhas seguidas.</span>}
            </div>
            {err && <div className="bc-error">{err}</div>}
          </>
        )}
      </section>

      <footer className="bcw-foot">
        <button className="bc-btn" onClick={onCancel}>Cancelar</button>
        <div className="bcw-foot-r">
          {step > 1 && <button className="bc-btn" onClick={() => setStep(step - 1)}>Voltar</button>}
          {step < 4
            ? <button className="bc-btn primary" disabled={!can[step - 1]} onClick={() => setStep(step + 1)}>Próximo <Icon name="arrRight" size={14} stroke={2.2} /></button>
            : <button className="bc-btn primary" disabled={saving || !count || (when === 'later' && !schedAt)} onClick={submit}>
                <Icon name="send" size={13} stroke={2} /> {saving ? 'Criando…' : when === 'later' ? 'Agendar disparo' : `Iniciar disparo para ${(count || 0).toLocaleString('pt-BR')}`}
              </button>}
        </div>
      </footer>
    </div>
  );
}

// Planilhas importadas — lista e remoção de lotes de leads importados
function BcImportListsTab() {
  const [lists, setLists] = React.useState(null); // null = carregando
  const [removing, setRemoving] = React.useState(null); // nome da lista sendo removida
  const [confirm, setConfirm] = React.useState(null); // nome pendente de confirmação
  const sb = window.HOROS_SB;

  const load = React.useCallback(async () => {
    const { data, error } = await sb.rpc('get_import_lists');
    if (!error) setLists(data || []);
  }, []);

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

  const doRemove = async (nome) => {
    setRemoving(nome);
    setConfirm(null);
    try {
      const { data, error } = await sb.rpc('archive_import_list', { p_lista_nome: nome });
      if (error || !data?.ok) throw new Error(error?.message || 'Erro desconhecido');
      window.dispatchEvent(new CustomEvent('horos:toast', {
        detail: { title: 'Lista removida', body: `${data.archived} lead${data.archived !== 1 ? 's' : ''} arquivados` }
      }));
      setLists(prev => (prev || []).filter(l => l.nome !== nome));
    } catch (e) {
      window.dispatchEvent(new CustomEvent('horos:toast', { detail: { title: 'Erro', body: e.message, type: 'error' } }));
    }
    setRemoving(null);
  };

  const fmtDate = (iso) => {
    if (!iso) return '';
    try { return new Date(iso).toLocaleDateString('pt-BR', { day: '2-digit', month: '2-digit', year: '2-digit' }); }
    catch { return ''; }
  };

  if (!lists) return <SkelList rows={4} avatar={36} right pad="14px 0" />;
  if (!lists.length) return (
    <div className="bc-empty" style={{ paddingTop: 48 }}>
      <div className="bc-empty-icon"><Icon name="sheet" size={28} /></div>
      <div className="bc-empty-title">Nenhuma planilha importada</div>
      <div className="bc-empty-text">Importe leads pelo CRM → Importar. Cada lista importada aparece aqui e você pode removê-la a qualquer momento.</div>
    </div>
  );
  return (
    <div className="bc-import-lists">
      <div className="bc-il-hint">Remover uma lista arquiva todos os leads dela — eles somem da sua tela, mas são preservados no sistema.</div>
      {lists.map(l => (
        <div key={l.nome} className="bc-il-row">
          <div className="bc-il-icon"><Icon name="sheet" size={16} /></div>
          <div className="bc-il-info">
            <div className="bc-il-nome">{l.nome}</div>
            <div className="bc-il-meta">
              {l.count} lead{l.count !== 1 ? 's' : ''}
              {l.por_nome ? ` · importada por ${l.por_nome}` : ''}
              {l.em ? ` · ${fmtDate(l.em)}` : ''}
            </div>
          </div>
          <div className="bc-il-actions">
            {confirm === l.nome ? (
              <>
                <span className="bc-il-confirm-text">Arquivar {l.count} lead{l.count !== 1 ? 's' : ''}?</span>
                <button className="bc-btn ghost" onClick={() => setConfirm(null)}>Cancelar</button>
                <button className="bc-btn danger" disabled={removing === l.nome} onClick={() => doRemove(l.nome)}>
                  {removing === l.nome ? 'Removendo…' : 'Confirmar'}
                </button>
              </>
            ) : (
              <button className="bc-btn ghost" onClick={() => setConfirm(l.nome)} disabled={removing === l.nome}>
                <Icon name="trash" size={13} /> Remover
              </button>
            )}
          </div>
        </div>
      ))}
    </div>
  );
}

// Contatos importados do Disparo: só Inbox e Arquivados. Não são leads e não aparecem no CRM
// até clicar em "Enviar para o CRM".
function BcContactsBoard() {
  const sb = window.HOROS_SB;
  const PAGE = 100;
  const [tab, setTab] = React.useState('inbox');
  const [lists, setLists] = React.useState([]);
  const [list, setList] = React.useState('');
  const [q, setQ] = React.useState('');
  const [rows, setRows] = React.useState(null);
  const [total, setTotal] = React.useState(0);
  const [sel, setSel] = React.useState(() => new Set());
  const [busy, setBusy] = React.useState(false);
  const [importing, setImporting] = React.useState(false);
  const toast = (title, body, type) => window.dispatchEvent(new CustomEvent('horos:toast', { detail: { title, body, type } }));

  const loadLists = React.useCallback(async () => {
    const { data } = await sb.rpc('broadcast_contact_lists');
    setLists(data || []);
  }, []);
  const load = React.useCallback(async (more) => {
    let qy = sb.from('broadcast_contacts').select('id,name,phone,email,cat,list_name,created_at', { count: 'exact' })
      .eq('status', tab).order('created_at', { ascending: false }).order('id');
    if (list) qy = qy.eq('list_name', list);
    const term = q.trim().replace(/[,()*%]/g, ' ').trim();
    if (term) {
      const dig = term.replace(/\D/g, '');
      qy = qy.or(dig.length >= 4 ? `name.ilike.*${term}*,phone_key.ilike.*${dig}*` : `name.ilike.*${term}*`);
    }
    const from = more ? (rows || []).length : 0;
    const { data, count } = await qy.range(from, from + PAGE - 1);
    setRows(more ? [...(rows || []), ...(data || [])] : (data || []));
    setTotal(count || 0);
  }, [tab, list, q, rows]);

  React.useEffect(() => { loadLists(); }, []);
  React.useEffect(() => { setSel(new Set()); const t = setTimeout(() => load(false), q ? 300 : 0); return () => clearTimeout(t); }, [tab, list, q]);

  const refresh = async () => { setSel(new Set()); await Promise.all([load(false), loadLists()]); };
  const ids = [...sel];
  const allOn = rows && rows.length > 0 && rows.every(r => sel.has(r.id));
  const toggle = (id) => setSel(prev => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
  const counts = lists.reduce((a, l) => ({ inbox: a.inbox + l.inbox, archived: a.archived + l.archived }), { inbox: 0, archived: 0 });
  const visibleLists = lists.filter(l => (tab === 'inbox' ? l.inbox : l.archived) > 0);

  const act = async (fn) => { if (busy || !ids.length) return; setBusy(true); try { await fn(); } finally { setBusy(false); } await refresh(); };
  const archive = (to) => act(async () => {
    const { error } = await sb.rpc('set_broadcast_contacts_status', { p_ids: ids, p_status: to });
    toast(error ? 'Erro' : (to === 'archived' ? 'Arquivados' : 'Voltaram para o Inbox'), error ? error.message : `${ids.length} contatos`, error ? 'error' : undefined);
  });
  const remove = () => act(async () => {
    if (!window.confirm(`Apagar ${ids.length} contatos? Isso não pode ser desfeito.`)) return;
    const { error } = await sb.rpc('delete_broadcast_contacts', { p_ids: ids });
    toast(error ? 'Erro' : 'Contatos apagados', error ? error.message : `${ids.length} contatos`, error ? 'error' : undefined);
  });
  const toCrm = () => act(async () => {
    if (!window.confirm(`Enviar ${ids.length} contatos para o CRM? Eles viram leads no Inbox do funil.`)) return;
    const chosen = rows.filter(r => sel.has(r.id));
    const payload = chosen.map(r => ({
      name: r.name || r.phone, initials: window.initials(r.name || '') || '?', // x[0] partia emoji ao meio → JSON inválido
      phone: r.phone, email: r.email || '', cat: r.cat || '', value: 0, seg: window.horosSeg ? window.horosSeg().id : 'imobiliario', lista: r.list_name,
    }));
    // Lotes de 500 (import_leads aceita até 2000 por chamada)
    let ins = 0, skip = 0;
    for (let i = 0; i < payload.length; i += 500) {
      const { data, error } = await sb.rpc('import_leads', { p_rows: payload.slice(i, i + 500), p_col: 'inbox' });
      if (error) { toast('Erro', error.message, 'error'); return; }
      ins += Array.isArray(data?.inserted) ? data.inserted.length : (Number(data?.inserted) || 0);
      skip += Number(data?.skipped) || 0;
    }
    await sb.rpc('delete_broadcast_contacts', { p_ids: chosen.map(r => r.id) });
    toast('Enviados para o CRM', `${ins} novos leads · ${skip} já existiam`);
    if (window.reloadLeads) window.reloadLeads();
  });

  return (
    <div className="bcc">
      <div className="bcc-head">
        <div className="bc-seg bcw-seg">
          <button className={tab === 'inbox' ? 'on' : ''} onClick={() => setTab('inbox')}>Inbox ({counts.inbox})</button>
          <button className={tab === 'archived' ? 'on' : ''} onClick={() => setTab('archived')}>Arquivados ({counts.archived})</button>
        </div>
        <select className="bcc-sel" value={list} onChange={e => setList(e.target.value)}>
          <option value="">Todas as planilhas</option>
          {visibleLists.map(l => <option key={l.nome} value={l.nome}>{l.nome} ({tab === 'inbox' ? l.inbox : l.archived})</option>)}
        </select>
        <input className="bcc-search" placeholder="Buscar nome ou telefone…" value={q} onChange={e => setQ(e.target.value)} />
        <button className="bc-btn primary" onClick={() => setImporting(true)}><Icon name="sheet" size={13} /> Importar planilha</button>
      </div>

      {ids.length > 0 && (
        <div className="bcc-bar">
          <b>{ids.length}</b> selecionados
          <span style={{ flex: 1 }} />
          {tab === 'inbox'
            ? <button className="bc-btn ghost" disabled={busy} onClick={() => archive('archived')}><Icon name="archive" size={13} /> Arquivar</button>
            : <button className="bc-btn ghost" disabled={busy} onClick={() => archive('inbox')}>Voltar para o Inbox</button>}
          <button className="bc-btn ghost" disabled={busy} onClick={toCrm}>Enviar para o CRM</button>
          <button className="bc-btn danger" disabled={busy} onClick={remove}><Icon name="trash" size={13} /> Apagar</button>
        </div>
      )}

      {rows === null ? <SkelList rows={5} avatar={30} right pad="12px 4px" />
        : rows.length === 0 ? (
          <div className="bc-empty" style={{ paddingTop: 36 }}>
            <div className="bc-empty-icon"><Icon name="sheet" size={28} /></div>
            <div className="bc-empty-title">{tab === 'inbox' ? 'Nenhum contato no Inbox' : 'Nenhum contato arquivado'}</div>
            <div className="bc-empty-text">Importe uma planilha: os contatos ficam aqui para disparo e só viram leads quando você mandar para o CRM.</div>
          </div>
        ) : (
          <div className="bcc-table">
            <div className="bcc-row bcc-th">
              <input type="checkbox" checked={allOn} onChange={() => setSel(allOn ? new Set() : new Set(rows.map(r => r.id)))} />
              <span>Nome</span><span>Telefone</span><span>Planilha</span>
            </div>
            {rows.map(r => (
              <label key={r.id} className={`bcc-row${sel.has(r.id) ? ' on' : ''}`}>
                <input type="checkbox" checked={sel.has(r.id)} onChange={() => toggle(r.id)} />
                <span className="bcc-name">{r.name || <i>Sem nome</i>}</span>
                <span>{fmtPhone(r.phone)}</span>
                <span className="bcc-list">{r.list_name}</span>
              </label>
            ))}
            {rows.length < total && <button className="bc-btn ghost" style={{ margin: '10px auto' }} onClick={() => load(true)}>Mostrar mais ({total - rows.length})</button>}
          </div>
        )}
      {importing && <BcContactsImport onClose={() => setImporting(false)} onDone={() => { setImporting(false); refresh(); }} />}
    </div>
  );
}

function BcContactsImport({ onClose, onDone }) {
  const [file, setFile] = React.useState(null);
  const [rows, setRows] = React.useState([]);
  const [name, setName] = React.useState('');
  const [busy, setBusy] = React.useState(false);
  const [err, setErr] = React.useState('');
  const pick = async (f) => {
    if (!f) return;
    setErr('');
    try {
      const r = await _bcReadSheet(f);
      if (!r.length) { setErr('Nenhum telefone encontrado na planilha.'); return; }
      setFile(f); setRows(r); setName(n => n || f.name.replace(/\.[^.]+$/, ''));
    } catch (e) { setErr(e.message || 'Não foi possível ler a planilha.'); }
  };
  const go = async () => {
    if (busy || !rows.length) return;
    setBusy(true); setErr('');
    let inserted = 0, skipped = 0;
    for (let i = 0; i < rows.length; i += 500) {
      const { data, error } = await window.HOROS_SB.rpc('import_broadcast_contacts', { p_rows: rows.slice(i, i + 500), p_list: name });
      if (error || !data?.ok) { setErr('Não foi possível importar. Tente de novo.'); setBusy(false); return; }
      inserted += data.inserted; skipped += data.skipped;
    }
    window.dispatchEvent(new CustomEvent('horos:toast', { detail: { title: 'Planilha importada', body: `${inserted} contatos no Inbox · ${skipped} repetidos ou inválidos` } }));
    onDone();
  };
  return ReactDOM.createPortal(
    <div className="arq-ov" style={{ zIndex: 10001 }} onMouseDown={e => { if (e.target === e.currentTarget && !busy) onClose(); }}>
      <div className="arq-modal" role="dialog" aria-label="Importar planilha">
        <div className="arq-head">
          <span className="arq-ic"><Icon name="sheet" size={20} /></span>
          <div style={{ minWidth: 0 }}><h3>Importar planilha</h3><p>Os contatos ficam no Inbox do disparo. Não viram leads nem aparecem no CRM.</p></div>
          <button className="arq-x" onClick={onClose} aria-label="Fechar"><Icon name="close" size={16} /></button>
        </div>
        <label className="arq-note"><span>Arquivo (XLSX, XLS ou CSV)</span>
          <input type="file" accept=".xlsx,.xls,.csv" onChange={e => pick(e.target.files[0])} /></label>
        {file && <>
          <p style={{ fontSize: 13, margin: '8px 0' }}>{rows.length.toLocaleString('pt-BR')} telefones lidos.</p>
          <label className="arq-note"><span>Nome da planilha</span>
            <input maxLength={80} value={name} onChange={e => setName(e.target.value)} /></label>
        </>}
        {err && <div className="arq-err">{err}</div>}
        <div className="arq-foot">
          <button className="arq-btn ghost" onClick={onClose} disabled={busy}>Cancelar</button>
          <button className="arq-btn" onClick={go} disabled={!rows.length || busy}>{busy ? 'Importando…' : 'Importar'}</button>
        </div>
      </div>
    </div>, document.body);
}

// Página dedicada de Disparos (broadcast.html)
function BroadcastPage() {
  const [theme, setTheme] = React.useState(localStorage.getItem('horos-theme') || 'dark');
  React.useEffect(() => {
    const h = e => setTheme(e.detail);
    window.addEventListener('horos:theme-change', h);
    return () => window.removeEventListener('horos:theme-change', h);
  }, []);
  return (
    <div className="app">
      <Sidebar active="broadcast" />
      <div className="main">
        <Topbar theme={theme} onThemeChange={setTheme} />
        <div className="content bc-fullpage">
          <BroadcastPanel pageMode={true} />
          <BcContactsBoard />
        </div>
        <MobileTabBar active="broadcast" />
      </div>
    </div>
  );
}

// broadcast.html → monta BroadcastPage no #root
// Demais telas → monta BroadcastRoot (modal overlay, evento horos:open-broadcasts)
(function mountBroadcast() {
  const isPage = /\/broadcast(\.html)?$/.test(location.pathname) || location.pathname === '/broadcast';
  const go = async () => {
    if (isPage) {
      const root = document.getElementById('root');
      if (root) ReactDOM.createRoot(root).render(<BroadcastPage />);
    } else {
      if (document.getElementById('horos-broadcast-root')) return;
      const el = document.createElement('div');
      el.id = 'horos-broadcast-root';
      document.body.appendChild(el);
      ReactDOM.createRoot(el).render(<BroadcastRoot />);
    }
  };
  if (document.body) go(); else document.addEventListener('DOMContentLoaded', go);
})();

window.BroadcastPanel = BroadcastPanel;
