// Produtos → Teste A/B (spec 2026-07-28-teste-ab-funis-design): compara duas fatias de
// vendas buygoods etapa a etapa, com normalização por cohort. Self-contained (molde
// Leads/DRE): filtros locais, fetch próprio, SEM mock — comparação inventada é dado falso.
//
// Layout (redesenho 28/jul): filtros e KPIs em DUAS colunas alinhadas (Referência à
// esquerda, Analisado à direita) → card de mix em largura cheia → TRÊS tabelas empilhadas,
// cada uma auto-contida com seus próprios rótulos de linha (Referência, Analisado,
// Veredito). Empilhar em largura cheia é o que garante zero rolagem lateral: 7 colunas
// numa tabela cabem folgado, 14 lado a lado não caberiam.
//
// Prefixo `abt` em TODOS os identificadores de topo: os .jsx dividem um único escopo de
// script (Babel standalone) e colisão de nome dá tela branca.

const ABT_SIDES = [
  { key: 'ref', label: 'Referência', color: 'var(--fenix-blue)' },
  { key: 'cur', label: 'Analisado',  color: 'var(--fenix-orange)' },
];

const ABT_TONE = {
  verde: 'var(--positive)', vermelho: 'var(--negative)',
  amarelo: 'var(--warning)', inconclusivo: 'var(--fg-4)',
};

// Palavra do semáforo de DEVOLUÇÃO (independente do de lucro — `confiancaDev` já vem do
// backend com o sinal invertido: devolver MENOS é "caiu", o verde). Nunca só a cor: o
// design da tela exige ponto + palavra, senão a leitura falha em daltonismo e no impresso.
const ABT_DEV_LABEL = {
  verde: 'caiu', vermelho: 'subiu', amarelo: 'sinal fraco', inconclusivo: 'inconclusivo',
};

// Dinheiro POR LEAD com 2 casas. O USD() do projeto arredonda para o inteiro — aqui os
// centavos são o dado: $0,74/lead × 2.700 leads = ~US$ 2.000 de diferença no funil.
// Respeita a moeda global (FX) igual aos formatadores oficiais.
const abtMoney = (v, signed) => {
  if (v == null || !isFinite(v)) return '—';
  const c = v * FX.rate;
  const sym = FX.code === 'BRL' ? 'R$ ' : '$';
  const loc = FX.code === 'BRL' ? 'pt-BR' : 'en-US';
  const sinal = signed ? (c >= 0 ? '+' : '−') : (c < 0 ? '−' : '');
  return sinal + sym + Math.abs(c).toLocaleString(loc,
    { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
const abtUsd = (v) => abtMoney(v, true);            // delta assinado
const abtPp  = (v) => (v == null ? '—' : `${v >= 0 ? '+' : '−'}${Math.abs(v).toFixed(1)} pp`);
// pValue PODE ser null e é caminho normal (pedidos da etapa acima da população exposta,
// ou algum lado sem amostra) — o backend devolve null de propósito, a UI mostra "—".
const abtP = (p) => (p == null ? '—' : p < 0.001 ? 'p<0,001' : `p=${p.toFixed(3)}`);
const abtWilson = (ic) => (ic ? `IC95% ${(ic[0] * 100).toFixed(1)}–${(ic[1] * 100).toFixed(1)}%`
                              : 'IC95% indisponível (população exposta não fecha)');

// Feedback visual direção-consciente (pedido do dono do produto, 30/jul): Δ% = (analisado
// − referência) ÷ |referência| × 100. `null` quando a referência é zero — percentual de
// nada não existe, e inventar um violaria o contrato de "nunca invente um número" da tela.
const abtDeltaPct = (delta, refVal) =>
  (delta == null || refVal == null || refVal === 0) ? null : (delta / Math.abs(refVal)) * 100;
const abtFmtPct = (pct) =>
  (pct == null ? '—' : `${pct >= 0 ? '+' : '−'}${Math.abs(pct).toFixed(1)}%`);
// Seta da variação BRUTA (nunca da "bondade" — isso é a cor). Vazia quando o delta é zero:
// não inventa direção onde não houve movimento.
const abtArrow = (delta) => (delta == null || delta === 0 ? '' : delta > 0 ? '▲' : '▼');

// Célula padrão "seta + valor + Δ%" repetida nas três aplicações (Veredito, Síntese,
// gaveta). `tone` é SEMPRE emprestado do semáforo que o backend já calculou
// (ABT_TONE[confianca] ou ABT_TONE[confiancaDev], já com o sinal de devolução invertido) —
// esta célula nunca re-deriva verde/vermelho. `muted` reduz a opacidade quando a linha é
// inconclusiva: a tela nunca finge confiança que o teste z não deu.
const AbtDelta = ({ value, delta, pct, tone, muted }) => (
  <span style={{ color: tone, opacity: muted ? .5 : 1, fontWeight: 700 }}>
    {abtArrow(delta) && <span aria-hidden="true">{abtArrow(delta)} </span>}
    {value}
    <span style={{ marginLeft: 4, fontWeight: 600, opacity: .82 }}>({abtFmtPct(pct)})</span>
  </span>
);

const abtFmtDia = (iso) => iso
  ? new Date(iso.slice(0, 10) + 'T12:00:00').toLocaleDateString('pt-BR',
      { day: '2-digit', month: 'short' })
  : '—';

// Colunas de métrica das tabelas de lado (Referência e Analisado). Uma lista só → os dois
// lados nunca saem de sincronia. Nas linhas de FRONT-END o "CVR" é o peso do pote no mix,
// não conversão — o cabeçalho da seção diz isso, e a célula carrega o sufixo "do mix".
const ABT_METRICS = [
  { key: 'orders',     label: 'Vendas',   fmt: (s) => NUM(s.orders) },
  { key: 'cvr',        label: 'CVR',      fmt: (s) => `${s.cvr.toFixed(1)}%` },
  { key: 'revenue',    label: 'Receita',  fmt: (s) => USD(s.revenue) },
  { key: 'devRate',  label: 'Reembolso + CGBK',   fmt: (s) => `${s.devRate.toFixed(1)}%` },
  { key: 'devLead',  label: 'Reemb+CGBK/lead', fmt: (s) => `$${s.devLead.toFixed(2)}` },
  { key: 'oppOrder',   label: 'OPP/ord',  fmt: (s) => abtMoney(s.oppOrder) },
  { key: 'oppLead',    label: 'OPP/lead', fmt: (s) => abtMoney(s.oppLead) },
];

const ABT_BLANK = { period: '30d', customRange: null, product: 'todos',
                    funnelTag: 'todos', affiliate: 'todos', refundBy: 'pedido' };

// Largura mínima do container para as tabelas de Referência e Analisado caberem LADO A
// LADO. Medido em 28/jul: com 6 métricas o conteúdo pede ~565px por tabela, então o par
// precisa de ~1.144px + o respiro. A folga até 1.240 cobre o dado crescer (receita na casa
// dos milhões, nome de etapa mais longo). Abaixo disso empilha — nunca corta nem rola,
// que é o requisito da tela.
const ABT_LADO_A_LADO_MIN = 1240;

// Filtros de UM lado. `tags` vem do payload do próprio lado (funis vistos na seleção).
// ATENÇÃO: `funnelTag` é o funil de MARKETING (b3/b4…), extraído do codename — NÃO é o
// param `funnel` do FilterBar global, que filtra variação de SKU. Nomes parecidos,
// filtros diferentes.
const AbtSideFilters = ({ side, value, patch, families, roster, tags }) => {
  // Se a tag escolhida sumiu da lista devolvida (trocou de produto, janela sem venda),
  // ela continua na lista: sem isso o Dropdown mostraria "—" com um filtro ativo.
  const tagOpts = [{ key: 'todos', label: 'Todos' },
    ...(tags || []).map(t => ({ key: t, label: String(t).toUpperCase() }))];
  if (value.funnelTag !== 'todos' && !tagOpts.some(o => o.key === value.funnelTag)) {
    tagOpts.push({ key: value.funnelTag, label: String(value.funnelTag).toUpperCase() });
  }
  return (
    <div style={{ ...abtx.sideCard, borderTop: `3px solid ${side.color}` }}>
      <div style={{ ...abtx.sideTag, color: side.color }}>{side.label}</div>
      <div style={abtx.filterRow}>
        <PeriodSelector period={value.period} setPeriod={(p) => patch({ period: p })}
          customRange={value.customRange} setCustomRange={(r) => patch({ customRange: r })} />
        <Dropdown icon="cart" label="Produto" value={value.product}
          onChange={(v) => patch({ product: v, funnelTag: 'todos' })}
          options={[{ key: 'todos', label: 'Todos' },
                    ...families.map(f => ({ key: f.name, label: f.name }))]} />
        <Dropdown icon="trending" label="Funil" value={value.funnelTag}
          onChange={(v) => patch({ funnelTag: v })} options={tagOpts} />
        <Dropdown icon="users" label="Afiliado" value={value.affiliate}
          onChange={(v) => patch({ affiliate: v })}
          options={[{ key: 'todos', label: 'Todos' }, ...roster]} />
        <Dropdown icon="refresh" label="Reembolso" value={value.refundBy}
          onChange={(v) => patch({ refundBy: v })}
          options={REFUND_BY_OPTIONS} />
      </div>
    </div>
  );
};

// KPI de um lado — mesma coluna e mesma ordem dos filtros, para os dois blocos alinharem.
// O número grande é o TOTAL de vendas (FE + BE), na régua do resto do dashboard
// (decisão 30/jul); os checkouts de FE viram contexto no subtítulo porque seguem
// sendo o denominador do OPP/lead e do mix — sumir com eles esconderia a base
// da normalização.
const AbtSideKpi = ({ side, dados }) => (
  <div style={{ ...abtx.kpiCard, borderLeft: `3px solid ${side.color}` }}>
    <div style={{ ...abtx.sideTag, color: side.color }}>{side.label}</div>
    <div style={abtx.kpiBig} className="fenix-num">
      {NUM(dados.vendasTotal)}<span style={abtx.kpiUnit}> vendas · front + back</span>
    </div>
    <div style={abtx.kpiSub} className="fenix-num">
      {abtFmtDia(dados.start)} — {abtFmtDia(dados.end)}
      <span style={abtx.kpiDias}> · {dados.dias} dia{dados.dias === 1 ? '' : 's'}
        · {NUM(dados.feCheckouts)} checkouts FE</span>
    </div>
  </div>
);

// Barras de mix de um lado (peso de cada pote no front-end).
// `maxPct` é o maior peso entre OS DOIS lados: escala compartilhada, senão o pote
// dominante de cada lado ficaria da mesma altura e a comparação — que é o ponto do
// bloco — mentiria. Altura útil de 46px; o mínimo de 3px mantém visível o pote de 1%.
const ABT_MIX_H = 46;
const AbtMixBars = ({ mix, color, maxPct }) => {
  if (!mix || !mix.length) return <div style={abtx.muted}>sem checkouts no período</div>;
  const teto = Math.max(maxPct || 0, 1);
  return (
    <div style={abtx.mixBars}>
      {mix.map(m => (
        <div key={m.bottles} style={{ flex: 1, textAlign: 'center', minWidth: 26 }}
          title={`${m.bottles} potes · ${NUM(m.orders)} checkouts · ${m.pesoPct.toFixed(1)}% do mix`}>
          <div style={{ height: Math.max(3, Math.round(ABT_MIX_H * m.pesoPct / teto)),
                        background: color, borderRadius: '3px 3px 0 0', opacity: .85 }} />
          <div style={abtx.mixLabel}>{m.bottles}p</div>
          <div style={abtx.mixPct} className="fenix-num">{m.pesoPct.toFixed(0)}%</div>
        </div>
      ))}
    </div>
  );
};

// Tabela padrão da tela: linhas de FRONT-END, respiro, faixa BACK-END, linhas de back-end.
// `linhas` = [{key, label, role, tag, celulas: [{v, style, title}]}]. Sem minWidth e sem
// overflowX: 7 colunas em largura cheia cabem — a tela não tem rolagem lateral.
const AbtTable = ({ eyebrow, color, cols, linhas, notaFe, notaBe, onRowClick }) => {
  const fe = linhas.filter(l => l.role === 'fe');
  const be = linhas.filter(l => l.role !== 'fe');
  const nCols = cols.length + 1;
  const faixa = (nome, nota) => (
    <tr>
      <td colSpan={nCols} style={abtx.faixa}>
        <span style={abtx.faixaNome}>{nome}</span>
        {nota && <span style={abtx.faixaNota}>{nota}</span>}
      </td>
    </tr>
  );
  // Só as linhas de BACK-END abrem a gaveta: a de front-end é o próprio mix, sem camada
  // nem diagnóstico próprio (mesmo critério do resto da tela).
  const linha = (l) => {
    const clicavel = !!onRowClick && l.role !== 'fe';
    return (
    <tr key={l.key}
      onClick={clicavel ? () => onRowClick(l) : undefined}
      style={clicavel ? { cursor: 'pointer' } : undefined}
      onMouseEnter={e => { e.currentTarget.style.background = 'var(--gray-50)'; }}
      onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; }}>
      <td style={abtx.tdLabel}>
        {l.label}
        {l.tag && <span style={abtx.priceTag} title={l.tagTitle}>{l.tag}</span>}
      </td>
      {l.celulas.map((c, i) => (
        <td key={i} className="fenix-num" title={c.title}
          style={{ ...abtx.tdNum, ...(c.style || {}) }}>{c.v}</td>
      ))}
    </tr>
    );
  };
  return (
    <div style={abtx.tableCard}>
      <div style={{ ...abtx.tableHead, borderLeft: `3px solid ${color}` }}>
        <span style={{ ...abtx.sideTag, color, marginBottom: 0 }}>{eyebrow}</span>
      </div>
      <table style={abtx.table}>
        <thead>
          <tr>
            <th style={{ ...abtx.th, textAlign: 'left' }}>Etapa</th>
            {cols.map(c => <th key={c} style={abtx.th}>{c}</th>)}
          </tr>
        </thead>
        <tbody>
          {fe.length > 0 && faixa('Front-end', notaFe)}
          {fe.map(linha)}
          {be.length > 0 && <tr style={abtx.respiro}><td colSpan={nCols} /></tr>}
          {be.length > 0 && faixa('Back-end', notaBe)}
          {be.map(linha)}
          {linhas.length === 0 && (
            <tr><td colSpan={nCols} style={abtx.empty}>
              Sem vendas nesta seleção — ajuste os filtros deste lado.
            </td></tr>
          )}
        </tbody>
      </table>
    </div>
  );
};

const AbTestView = ({ families = [], rosterAll = [] }) => {
  const [ref, setRef] = React.useState(ABT_BLANK);
  const [cur, setCur] = React.useState(ABT_BLANK);
  const [data, setData] = React.useState(null);
  const [err, setErr] = React.useState(false);
  const [etapaAberta, setEtapaAberta] = React.useState(null);
  const fullLoading = useFullLoading();
  // useWidth é o ResizeObserver que o projeto já usa no Charts.jsx — mede o container
  // real, não a viewport, então acerta mesmo com a sidebar aberta/fechada.
  const [wrapRef, wrapW] = useWidth();
  const ladoALado = wrapW >= ABT_LADO_A_LADO_MIN;

  // mark() em TODO setter de filtro: é o que faz o FullLoadingOverlay aparecer só quando
  // a troca veio do usuário (molde spec 2026-07-23).
  const patchRef = (p) => { fullLoading.mark(); setRef(s => ({ ...s, ...p })); };
  const patchCur = (p) => { fullLoading.mark(); setCur(s => ({ ...s, ...p })); };
  const espelhar = () => { fullLoading.mark(); setCur({ ...ref }); };
  const retry = useAutoRetry(err);   // falhou → tenta de novo a cada 15s, sozinho

  React.useEffect(() => {
    const began = fullLoading.begin();
    let cancelled = false;
    fetchAbTestAPI({ ref, cur })
      .then(d => { if (!cancelled) { setData(d); setErr(false); } })
      .catch(() => { if (!cancelled) setErr(true); })
      .finally(() => { if (!cancelled) fullLoading.end(began); });
    return () => { cancelled = true; };
  }, [ref, cur, retry]);

  // Índices por chave de etapa: a gaveta precisa das linhas CRUAS dos dois lados, e
  // `cmp.stages` só carrega a comparação.
  const refPorKey = React.useMemo(
    () => Object.fromEntries((data?.ref?.stages || []).map((s) => [s.key, s])), [data]);
  const curPorKey = React.useMemo(
    () => Object.fromEntries((data?.cur?.stages || []).map((s) => [s.key, s])), [data]);

  // Dropdown de afiliado — `rosterAll` é o roster do /affiliates SEM os filtros globais
  // (esta tela esconde o FilterBar), mesmo critério da DRE.
  const affiliateOptions = React.useMemo(() => {
    const seen = new Set(); const opts = [];
    rosterAll.forEach(r => {
      if (r.name && r.name !== 'Não atribuído' && !seen.has(r.name)) {
        seen.add(r.name); opts.push({ key: r.name, label: r.name });
      }
    });
    return opts;
  }, [rosterAll]);

  const sortedFamilies = React.useMemo(
    () => [...families].sort((a, b) => a.name.localeCompare(b.name)), [families]);

  // Os dois lados nascem idênticos: a tela abre comparando a fatia com ela mesma, tudo
  // zerado. Sem esta dica o primeiro render parece defeito.
  const espelhados = React.useMemo(
    () => JSON.stringify(ref) === JSON.stringify(cur), [ref, cur]);

  // O wrapper com `wrapRef` precisa existir em TODOS os estados: se ele só aparecesse com
  // dado, o ResizeObserver do useWidth montaria com a ref nula e nunca mediria (o efeito
  // tem deps []), e a tela ficaria presa no layout empilhado mesmo sobrando largura.
  // 1º load falhou: LoadingScreen offline + retry automático — nunca card estático
  // sem retry (requisito ui-fenix, spec 2026-07-30-reconexao). O wrapper mantém o
  // ref do useWidth: o ResizeObserver monta com deps [] e precisa do nó desde já.
  if (err && !data) return (
    <div ref={wrapRef}>
      <LoadingScreen conn="offline" />
      <div style={abtx.reconNota}>
        A comparação não tem versão de demonstração — nenhum número inventado entra
        aqui. Reconectando sozinha a cada 15s.
      </div>
    </div>
  );
  // Primeiro carregamento: tela padrão do projeto.
  if (!data) return <div ref={wrapRef}><LoadingScreen conn="connecting" /></div>;

  const cmp = data.comparison;
  const sides = { ref: data.ref, cur: data.cur };
  const stageOf = (sideKey, key) => sides[sideKey].stages.find(s => s.key === key) || null;
  const falhas = (cmp.selfChecks || []).filter(c => !c.ok);
  const tot = cmp.totals;
  const sep = tot.separacao;
  // A cascata visual (4 termos: mix CVR, mix estrutura, conversão, devolução) mora no
  // componente AbtCascata, acima das tabelas — ela substitui o antigo resumo de 3 termos
  // que ficava aqui embaixo, porque `efeitoReal` é justamente o que a cisão em
  // efeitoConversao/efeitoDevolucao (Task 9) precisava abrir.

  // Escala compartilhada das barras de mix: o maior peso entre os dois lados.
  const maxPeso = Math.max(
    ...ABT_SIDES.flatMap(s => (sides[s.key].mix || []).map(m => m.pesoPct)), 1);

  // --- linhas das três tabelas, todas na MESMA ordem de etapa (a do backend) ---
  const NOTA_FE = 'a coluna CVR mostra o peso do pote no mix, não conversão';
  const NOTA_BE = 'CVR = pedidos ÷ população exposta à etapa';

  const linhasDoLado = (sideKey) => cmp.stages.map(st => {
    const s = stageOf(sideKey, st.key);
    const z = st.zTest || {};
    const isFe = st.role === 'fe';
    const celulas = ABT_METRICS.map(m => {
      if (!s) return { v: '—' };
      // CVR ganha marca de alerta quando os pedidos da etapa superam a população exposta:
      // a taxa é impossível (>100%) e não pode ser lida como número normal.
      const invalida = m.key === 'cvr' && st.popInvalida && s.orders > s.popExposta;
      const wl = sideKey === 'ref' ? z.wilsonRef : z.wilsonCur;
      const title = m.key !== 'cvr' ? undefined
        : invalida
          ? `CVR impossível: ${NUM(s.orders)} pedidos para ${NUM(s.popExposta)} expostos. `
            + 'Parte do back-end veio de um front-end fora da janela — a taxa desta linha '
            + 'não é comparável.'
          : isFe
            ? `${NUM(s.orders)} de ${NUM(sides[sideKey].feCheckouts)} checkouts de front-end`
            : `${NUM(s.orders)} de ${NUM(s.popExposta)} expostos · ${abtWilson(wl)}`;
      return {
        v: (m.key === 'cvr' && isFe)
          ? <span>{m.fmt(s)}<span style={abtx.sufixo}> do mix</span></span>
          : <span>{m.fmt(s)}{invalida ? ' ⚠' : ''}</span>,
        title,
        style: invalida ? { color: 'var(--warning)', fontWeight: 700, cursor: 'help' }
              : (m.key === 'cvr' && isFe) ? { cursor: 'help' } : undefined,
      };
    });
    return { key: st.key, label: st.label, role: st.role, celulas };
  });

  const linhasVeredito = cmp.stages.map(st => {
    const r = stageOf('ref', st.key);
    const z = st.zTest || {};
    const isFe = st.role === 'fe';
    const tone = ABT_TONE[st.confianca] || ABT_TONE.inconclusivo;
    // Δ CVR normalizado: o CVR do lado analisado sob o mix da REFERÊNCIA, menos o CVR da
    // referência. É o número que o bruto ao lado pode estar distorcendo — mostrar os dois
    // lado a lado é o ponto da tela. Não se aplica a linha de mix.
    const cvrNorm = st.camadas && st.camadas.cvrNorm;
    const deltaNorm = (!isFe && r && cvrNorm != null) ? cvrNorm - r.cvr : null;
    // Semáforo de DEVOLUÇÃO — independente do de lucro (`confiancaDev` já vem com o
    // sinal invertido do backend). É o par que importa: lucro verde com devolução
    // vermelha é exatamente o caso que este eixo existe para capturar.
    const toneDev = ABT_TONE[st.confiancaDev] || ABT_TONE.inconclusivo;
    const zDev = st.zTestDev || {};
    return {
      key: st.key, label: st.label, role: st.role,
      tag: st.precoMudou ? 'preço' : null,
      tagTitle: 'O preço desta etapa mudou entre os dois lados — a comparação de '
        + 'comportamento cai para C2.',
      celulas: [
        { v: st.camadaPrincipal
            ? <span style={abtx.camada}>{st.camadaPrincipal}</span>
            : <span style={abtx.muted}>—</span>,
          title: st.razao, style: { cursor: 'help', textAlign: 'center' } },
        { v: isFe
            ? <span style={abtx.neutral}>{abtPp(st.deltaCvrPp)}</span>
            : <AbtDelta value={abtPp(st.deltaCvrPp)} delta={st.deltaCvrPp} tone={tone}
                muted={st.confianca === 'inconclusivo'}
                pct={abtDeltaPct(st.deltaCvrPp, r && r.cvr)} />,
          title: isFe ? 'Quanto o peso deste pote no mix mudou entre os dois lados — nem '
                        + 'melhor nem pior, só diferente (linha de mix é neutra).'
                      : 'Delta do CVR BRUTO — pode estar contaminado pela mudança de mix. '
                        + 'Compare com a coluna normalizada ao lado.' },
        { v: deltaNorm == null
            ? '—'
            : <AbtDelta value={abtPp(deltaNorm)} delta={deltaNorm} tone={tone}
                muted={st.confianca === 'inconclusivo'}
                pct={abtDeltaPct(deltaNorm, r && r.cvr)} />,
          title: deltaNorm == null
            ? 'Linha de mix — não há CVR normalizado a comparar.'
            : 'Delta do CVR já normalizado pelo mix da referência: é a mudança de '
              + 'comportamento, sem o efeito de ter entrado público diferente.' },
        { v: isFe
            ? <span style={abtx.neutral}>{abtUsd(st.deltaOppLead)}</span>
            : <AbtDelta value={abtUsd(st.deltaOppLead)} delta={st.deltaOppLead} tone={tone}
                muted={st.confianca === 'inconclusivo'} pct={st.deltaOppLeadPct} />,
          title: isFe
            ? 'Linha de mix: só se mexe se o ticket do próprio front-end mudou — não é '
              + 'veredito de melhora/piora, por isso fica neutro.'
            : 'Contribuição desta etapa para o total normalizado.' },
        { v: abtP(z.pValue), style: { color: tone },
          title: z.pValue == null
            ? 'Sem teste z: algum lado ficou sem amostra ou os pedidos da etapa superam a população exposta.'
            : st.confianca === 'inconclusivo'
              ? 'Inconclusivo: diferença dentro do ruído a 95% ou volume insuficiente (n < 10 em algum lado).'
              : (z.significant ? 'Diferença significativa a 95%.' : 'Diferença dentro do ruído a 95%.') },
        { v: <span style={{ ...abtx.devCel, opacity: st.confiancaDev === 'inconclusivo' ? .5 : 1 }}>
               {abtArrow(st.deltaDevLead) && <span style={{ color: toneDev }}>{abtArrow(st.deltaDevLead)}</span>}
               <span style={{ color: toneDev, fontWeight: 700 }}>{abtUsd(st.deltaDevLead)}</span>
               <span style={{ ...abtx.sufixo, marginLeft: 0 }}>
                 ({abtFmtPct(abtDeltaPct(st.deltaDevLead, r && r.devLead))})
               </span>
               <span style={abtx.devPalavra}>{ABT_DEV_LABEL[st.confiancaDev] || ABT_DEV_LABEL.inconclusivo}</span>
             </span>,
          title: `Reembolso + CGBK: ${abtPp(st.deltaDevRatePp)} · ${abtP(zDev.pValue)} · semáforo `
            + 'independente do lucro — devolver menos é sempre "caiu", mesmo com o lucro piorando.' },
      ],
    };
  });

  return (
    <div ref={wrapRef} style={{ position: 'relative' }}>
      <div style={abtx.header}>
        <div>
          <div style={abtx.eye}>Produtos</div>
          <div style={abtx.title}>Teste A/B de funis</div>
        </div>
        <button style={abtx.mirror} onClick={espelhar}
          title="Copia os filtros da Referência para o lado Analisado">
          Espelhar filtros →
        </button>
      </div>

      {err && (
        <div style={abtx.notice}><Icon name="alert" size={14} color="currentColor" />
          Falha ao atualizar — mostrando a última comparação carregada.</div>
      )}

      {/* filtros e KPIs na MESMA grade de 2 colunas: o que está embaixo de cada filtro é
          o resultado daquele filtro. */}
      <div style={abtx.doisLados}>
        <AbtSideFilters side={ABT_SIDES[0]} value={ref} patch={patchRef}
          families={sortedFamilies} roster={affiliateOptions} tags={data.ref.funnelTags} />
        <AbtSideFilters side={ABT_SIDES[1]} value={cur} patch={patchCur}
          families={sortedFamilies} roster={affiliateOptions} tags={data.cur.funnelTags} />
      </div>
      <div style={abtx.doisLados}>
        <AbtSideKpi side={ABT_SIDES[0]} dados={data.ref} />
        <AbtSideKpi side={ABT_SIDES[1]} dados={data.cur} />
      </div>

      {espelhados && (
        <div style={abtx.dica}>
          <Icon name="alert" size={14} color="currentColor" />
          Os dois lados estão com os mesmos filtros, então tudo dá zero. Mude o funil, o
          período ou o produto de um dos lados para começar a comparação.
        </div>
      )}

      {/* Mix em card próprio: é ele que explica por que o número bruto pode enganar, então
          vem antes das tabelas. */}
      <div style={abtx.mixCard}>
        <div style={abtx.mixTopo}>
          <div>
            <div style={abtx.eye}>Mix de front-end</div>
            <div style={abtx.mixTexto}>
              Quem compra 6 potes se comporta diferente de quem compra 2. Se o mix mudou, a
              comparação bruta engana — é isso que as camadas corrigem.
            </div>
          </div>
          <div style={abtx.mixShift}>
            <div style={abtx.synthEye}>Mudança de mix</div>
            <div style={abtx.mixShiftNum} className="fenix-num">
              {cmp.mixShiftPp.toFixed(1)} pp
            </div>
            <div style={{ ...abtx.badge,
                          background: cmp.modo === 'cohort_completo' ? 'var(--positive-bg)' : 'var(--warning-bg)',
                          color: cmp.modo === 'cohort_completo' ? 'var(--positive)' : 'var(--warning)' }}
              title={cmp.modo === 'cohort_completo'
                ? 'Praticamente todo back-end foi ligado ao front-end que abriu o funil.'
                : 'Parte do back-end não tem cohort (o FE ficou fora da janela) — a normalização por mix perde precisão.'}>
              {cmp.modo === 'cohort_completo' ? 'COHORT COMPLETO' : 'COHORT PARCIAL'}
            </div>
          </div>
        </div>
        <div style={abtx.doisLados}>
          {ABT_SIDES.map(s => (
            <div key={s.key}>
              <div style={{ ...abtx.sideTag, color: s.color }}>{s.label}</div>
              <AbtMixBars mix={sides[s.key].mix} color={s.color} maxPct={maxPeso} />
            </div>
          ))}
        </div>
      </div>

      {/* Cascata (spec 2026-07-30): entra ANTES das tabelas — o card de mix explica por
          que o número bruto engana, a cascata dá a resposta corrigida, e as três tabelas
          abaixo são a trilha de auditoria daquele número. */}
      <AbtCascata totals={cmp.totals} />

      {/* Lado a lado quando há largura para as duas tabelas inteiras; empilhado quando
          não. Cortar conteúdo ou criar rolagem lateral não são opções nesta tela. */}
      <div style={ladoALado ? abtx.doisLados : undefined}>
        <AbtTable eyebrow="Referência" color={ABT_SIDES[0].color}
          cols={ABT_METRICS.map(m => m.label)} linhas={linhasDoLado('ref')}
          notaFe={NOTA_FE} notaBe={NOTA_BE} />
        <AbtTable eyebrow="Analisado" color={ABT_SIDES[1].color}
          cols={ABT_METRICS.map(m => m.label)} linhas={linhasDoLado('cur')}
          notaFe={NOTA_FE} notaBe={NOTA_BE} />
      </div>

      <AbtTable eyebrow="Veredito" color="var(--fg-2)"
        cols={['Camada', 'Δ CVR bruto', 'Δ CVR normalizado', 'Δ OPP/lead', 'Significância', 'Reemb+CGBK/lead']}
        linhas={linhasVeredito}
        notaFe="o veredito da linha de mix é a mudança de peso, em pp"
        notaBe="cor só quando a diferença é significativa a 95% E há volume"
        onRowClick={(linha) => setEtapaAberta(cmp.stages.find(s => s.key === linha.key) || null)} />

      <div style={abtx.footNote}>
        <b>OPP</b> = lucro operacional (OPP/ord por pedido, OPP/lead por checkout de
        front-end){data.costs && <span title={`Régua da família ${data.costs.base || '—'}, congelada na Referência e aplicada igual aos dois lados.`}>
          {' '}— receita menos COGS de {abtMoney(data.costs.cogsPerBottle)}/frasco, frete de{' '}
          {abtMoney(data.costs.shipFe)} (só no front-end) e a devolução OBSERVADA de cada
          etapa×cohort — não uma reserva fixa</span>}. A coluna Δ OPP/lead soma exatamente {abtUsd(tot.oppLeadCur - tot.oppLeadRef)} —
        a diferença até o <b>normalizado</b> ({abtMoney(tot.oppLeadCur)}/lead), não até o que
        de fato aconteceu ({abtMoney(tot.oppLeadCurBruto)}/lead); a decomposição do número real
        está na síntese abaixo. Linha em cinza = inconclusiva; CVR em âmbar com ⚠ = população
        exposta não fecha (back-end vindo de um front-end fora da janela). A coluna
        Reemb+CGBK/lead tem semáforo PRÓPRIO — lucro verde com devolução vermelha é o par que
        importa: significa que o resultado veio à custa de mais reembolso (só reembolso e
        chargeback nesta tela — cancelamento fica fora, decisão de produto de 30/jul/2026).
        Linha de back-end:
        clique para abrir o diagnóstico da etapa.
      </div>

      {/* síntese: os TRÊS ancoradouros (a cascata em si já está acima, antes das tabelas) */}
      <div style={abtx.synthCard}>
        <div style={abtx.eye}>Síntese</div>
        <div style={abtx.synthRow}>
          <div>
            <div style={abtx.synthEye}>OPP/lead · referência</div>
            <div style={abtx.synthBig} className="fenix-num">{abtMoney(tot.oppLeadRef)}</div>
            <div style={abtx.synthSub}>a régua da comparação</div>
          </div>
          <div>
            <div style={abtx.synthEye}>OPP/lead · analisado (real)</div>
            <div style={abtx.synthBig} className="fenix-num">{abtMoney(tot.oppLeadCurBruto)}</div>
            {/* Comparativo contra a referência (spec 2026-07-30-feedback-visual): mesma
                régua direção-consciente do resto da tela, sem confiança estatística própria
                nesta síntese (é soma, não teste), por isso sem opacidade reduzida. */}
            <div style={abtx.synthDelta}>
              <AbtDelta value={abtUsd(tot.oppLeadCurBruto - tot.oppLeadRef)}
                delta={tot.oppLeadCurBruto - tot.oppLeadRef}
                pct={abtDeltaPct(tot.oppLeadCurBruto - tot.oppLeadRef, tot.oppLeadRef)}
                tone={tot.oppLeadCurBruto >= tot.oppLeadRef ? 'var(--positive)' : 'var(--negative)'} />
            </div>
            <div style={abtx.synthSub}>o que de fato aconteceu na janela</div>
          </div>
          <div>
            <div style={abtx.synthEye}>OPP/lead · normalizado ({tot.camadaPrincipal})</div>
            <div style={abtx.synthBig} className="fenix-num">{abtMoney(tot.oppLeadCur)}</div>
            <div style={abtx.synthDelta}>
              <AbtDelta value={abtUsd(tot.oppLeadCur - tot.oppLeadRef)}
                delta={tot.oppLeadCur - tot.oppLeadRef}
                pct={abtDeltaPct(tot.oppLeadCur - tot.oppLeadRef, tot.oppLeadRef)}
                tone={tot.oppLeadCur >= tot.oppLeadRef ? 'var(--positive)' : 'var(--negative)'} />
            </div>
            <div style={abtx.synthSub}>se o público fosse o mesmo da referência</div>
          </div>
          <div>
            <div style={abtx.synthEye}>Efeito real (sem mix)</div>
            <div style={{ ...abtx.synthBig,
                          color: sep.efeitoReal >= 0 ? 'var(--positive)' : 'var(--negative)' }}
              className="fenix-num">
              {abtArrow(sep.efeitoReal) && <span aria-hidden="true">{abtArrow(sep.efeitoReal)} </span>}
              {abtUsd(sep.efeitoReal)}
            </div>
            <div style={abtx.synthDelta}>
              <span style={{ color: sep.efeitoReal >= 0 ? 'var(--positive)' : 'var(--negative)',
                            fontWeight: 600, opacity: .82 }}>
                ({abtFmtPct(abtDeltaPct(sep.efeitoReal, tot.oppLeadRef))} da referência)
              </span>
            </div>
            <div style={abtx.synthSub}>por lead, tirando a mudança de público</div>
          </div>
        </div>
        <div style={abtx.muted}>{tot.razao}</div>

        {/* Só o texto do alerta — os tipos (populacao_invalida, sobreposicao,
            cohort_sumido, modo_parcial, volume, taxa_diaria, concentracao) são do
            backend e podem mudar; a UI não os hardcoda. */}
        {(cmp.alerts || []).map((a, i) => (
          <div key={i} style={abtx.alert}>
            <Icon name="alert" size={13} color="currentColor" />{a.texto}
          </div>
        ))}
        {falhas.length > 0 && (
          <div style={abtx.checkFail} title={falhas.map(f => `${f.nome}: ${f.detalhe}`).join('\n')}>
            {falhas.length} conferência(s) interna(s) não fecharam — passe o mouse para ver.
          </div>
        )}
      </div>

      {etapaAberta && refPorKey[etapaAberta.key] && curPorKey[etapaAberta.key] && (
        // `refEtapa`, não `ref` — nome reservado do React (ver comentário no topo do
        // AbtStageDrawer): um prop JSX chamado `ref` nunca chega em `props`.
        <AbtStageDrawer etapa={etapaAberta}
          refEtapa={refPorKey[etapaAberta.key]}
          cur={curPorKey[etapaAberta.key]}
          onClose={() => setEtapaAberta(null)} />
      )}

      <FullLoadingOverlay show={fullLoading.show && !!data} conn="connecting" />
    </div>
  );
};

const abtx = {
  header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, marginBottom: 16, flexWrap: 'wrap' },
  eye: { font: 'var(--text-label)', letterSpacing: '.05em', textTransform: 'uppercase', color: 'var(--fg-3)' },
  title: { font: '800 22px/1.1 var(--font-sans)', color: 'var(--fg-1)', letterSpacing: '-.01em', marginTop: 4 },
  mirror: { border: '1px solid var(--border-2)', background: '#fff', color: 'var(--fg-2)', borderRadius: 'var(--r-md)', padding: '9px 14px', font: '600 12.5px/1 var(--font-sans)', cursor: 'pointer' },
  // grade única de 2 colunas: filtros, KPIs e barras de mix usam a MESMA, então tudo
  // que é de um lado fica sempre na mesma coluna vertical.
  doisLados: { display: 'grid', gridTemplateColumns: 'repeat(2,minmax(0,1fr))', gap: 14, marginBottom: 14 },
  sideCard: { background: '#fff', border: '1px solid var(--border-1)', borderRadius: 'var(--r-lg)', boxShadow: 'var(--shadow-sm)', padding: '14px 16px' },
  sideTag: { font: '800 10.5px/1 var(--font-sans)', letterSpacing: '.1em', textTransform: 'uppercase', marginBottom: 10 },
  filterRow: { display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' },
  kpiCard: { background: '#fff', border: '1px solid var(--border-1)', borderRadius: 'var(--r-lg)', boxShadow: 'var(--shadow-sm)', padding: '13px 16px' },
  kpiBig: { font: '800 24px/1.1 var(--font-sans)', color: 'var(--fg-1)' },
  kpiUnit: { font: '600 11.5px/1 var(--font-sans)', color: 'var(--fg-3)' },
  kpiSub: { font: '500 11.5px/1.4 var(--font-num)', color: 'var(--fg-3)', marginTop: 5 },
  kpiDias: { color: 'var(--fg-4)' },
  dica: { display: 'flex', alignItems: 'center', gap: 7, font: '600 12px/1.4 var(--font-sans)', color: 'var(--info, #303880)', background: 'var(--bg-sunken)', border: '1px solid var(--border-1)', padding: '10px 14px', borderRadius: 'var(--r-md)', marginBottom: 14 },
  mixCard: { background: '#fff', border: '1px solid var(--border-1)', borderRadius: 'var(--r-lg)', boxShadow: 'var(--shadow-sm)', padding: '16px 20px 14px', marginBottom: 16 },
  mixTopo: { display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 20, marginBottom: 14, flexWrap: 'wrap' },
  mixTexto: { font: 'var(--text-body-sm)', color: 'var(--fg-3)', maxWidth: 620, marginTop: 4 },
  mixShift: { textAlign: 'right', flex: 'none' },
  mixShiftNum: { font: '800 22px/1.1 var(--font-sans)', color: 'var(--fg-1)', marginTop: 3 },
  mixBars: { display: 'flex', gap: 6, alignItems: 'flex-end', height: 66 },
  mixLabel: { font: '700 10px/1.6 var(--font-sans)', color: 'var(--fg-3)' },
  mixPct: { font: '700 11px/1.2 var(--font-num)', color: 'var(--fg-2)' },
  badge: { display: 'inline-block', marginTop: 7, padding: '4px 9px', borderRadius: 'var(--r-sm)', font: '800 10px/1 var(--font-sans)', letterSpacing: '.06em', cursor: 'help' },
  muted: { font: 'var(--text-body-sm)', color: 'var(--fg-3)' },
  // Tom neutro do feedback direção-consciente: linha de FE não é boa nem ruim, só
  // diferente — nunca pinta de verde/vermelho (spec 2026-07-30-feedback-visual).
  neutral: { color: 'var(--fg-3)', fontWeight: 700 },
  notice: { display: 'flex', alignItems: 'center', gap: 6, font: '600 12px/1.4 var(--font-sans)', color: 'var(--warning)', background: 'var(--warning-bg)', padding: '10px 16px', borderRadius: 'var(--r-md)', marginBottom: 12 },
  // Tabela: SEM minWidth e SEM overflowX — em largura cheia as 6-7 colunas cabem, e a
  // tela não deve ter rolagem lateral em hipótese nenhuma.
  tableCard: { background: '#fff', border: '1px solid var(--border-1)', borderRadius: 'var(--r-lg)', boxShadow: 'var(--shadow-sm)', overflow: 'hidden', marginBottom: 14 },
  tableHead: { padding: '12px 16px 10px', borderBottom: '1px solid var(--border-1)' },
  table: { width: '100%', borderCollapse: 'collapse', tableLayout: 'auto' },
  th: { font: '700 10px/1 var(--font-sans)', letterSpacing: '.04em', textTransform: 'uppercase', color: 'var(--fg-3)', padding: '9px 10px', background: 'var(--bg-sunken)', borderBottom: '1px solid var(--border-2)', whiteSpace: 'nowrap', textAlign: 'right' },
  faixa: { padding: '7px 14px', background: 'var(--bg-sunken)', borderBottom: '1px solid var(--border-1)' },
  faixaNome: { font: '800 10px/1 var(--font-sans)', letterSpacing: '.1em', textTransform: 'uppercase', color: 'var(--fg-2)' },
  faixaNota: { font: '500 11px/1 var(--font-sans)', color: 'var(--fg-4)', marginLeft: 10 },
  respiro: { height: 10 },
  tdLabel: { padding: '11px 14px', borderBottom: '1px solid var(--border-1)', font: 'var(--text-body-sm)', color: 'var(--fg-1)', whiteSpace: 'nowrap', fontWeight: 700 },
  tdNum: { padding: '11px 10px', borderBottom: '1px solid var(--border-1)', font: 'var(--text-body-sm)', color: 'var(--fg-1)', whiteSpace: 'nowrap', textAlign: 'right', fontFamily: 'var(--font-num)', fontVariantNumeric: 'tabular-nums' },
  sufixo: { font: '500 10px/1 var(--font-sans)', color: 'var(--fg-4)', marginLeft: 3 },
  camada: { display: 'inline-block', padding: '3px 7px', borderRadius: 'var(--r-sm)', background: 'var(--fenix-blue)', color: '#fff', font: '800 10px/1 var(--font-sans)' },
  // Semáforo de devolução na tabela de Veredito: seta colorida + valor + Δ% + palavra —
  // nunca só a cor (spec 2026-07-30-feedback-visual substituiu o ponto pela seta ▲/▼).
  devCel: { display: 'inline-flex', alignItems: 'center', gap: 5, justifyContent: 'flex-end' },
  devPalavra: { font: '600 9.5px/1 var(--font-sans)', color: 'var(--fg-4)', textTransform: 'lowercase' },
  priceTag: { display: 'inline-block', marginLeft: 7, padding: '2px 6px', borderRadius: 'var(--r-sm)', background: 'var(--warning-bg)', color: 'var(--warning)', font: '800 9px/1.3 var(--font-sans)', letterSpacing: '.04em', textTransform: 'uppercase', cursor: 'help' },
  empty: { padding: '34px 22px', textAlign: 'center', font: 'var(--text-body-sm)', color: 'var(--fg-4)' },
  footNote: { margin: '2px 2px 16px', font: '500 11.5px/1.5 var(--font-sans)', color: 'var(--fg-3)' },
  synthCard: { background: '#fff', border: '1px solid var(--border-1)', borderRadius: 'var(--r-lg)', boxShadow: 'var(--shadow-sm)', padding: '16px 20px' },
  synthRow: { display: 'grid', gridTemplateColumns: 'repeat(4,minmax(0,1fr))', gap: 20, margin: '10px 0 8px' },
  synthEye: { font: 'var(--text-label)', letterSpacing: '.04em', textTransform: 'uppercase', color: 'var(--fg-3)' },
  synthBig: { font: '800 22px/1.1 var(--font-sans)', color: 'var(--fg-1)', marginTop: 4 },
  // Linha de feedback direção-consciente sob o número absoluto do tile — mesma seta +
  // valor + Δ% do resto da tela, tom pequeno para não competir com o número grande.
  synthDelta: { font: '700 12px/1.3 var(--font-num)', marginTop: 3 },
  synthSub: { font: '500 11px/1.4 var(--font-sans)', color: 'var(--fg-4)', marginTop: 3 },
  alert: { display: 'flex', alignItems: 'flex-start', gap: 6, marginTop: 10, font: '600 12px/1.4 var(--font-sans)', color: 'var(--warning)', background: 'var(--warning-bg)', padding: '9px 14px', borderRadius: 'var(--r-md)' },
  checkFail: { marginTop: 10, font: '600 11.5px/1.4 var(--font-sans)', color: 'var(--warning)', cursor: 'help' },
  reconNota: { textAlign: 'center', font: 'var(--text-body-sm)', color: 'var(--fg-4)', maxWidth: 520, margin: '-28px auto 0' },
};

window.AbTestView = AbTestView;
