// ============================================================
// Grupo Fênix — Dashboard de Afiliados · data + helpers
// Plataformas reais: Buygoods, Clickbank, Digistore24, PagAmerican
// ============================================================

const Icon = ({ name, size = 20, color = 'currentColor', sw = 1.9, style }) => {
  const P = {
    dashboard: <><rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/></>,
    users: <><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.9"/></>,
    chart: <><path d="M3 3v18h18"/><path d="M7 14l4-4 3 3 5-6"/></>,
    bolt: <path d="M13 2 3 14h8l-1 8 10-12h-8z"/>,
    cart: <><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></>,
    mail: <><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-10 6L2 7"/></>,
    sms: <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>,
    dollar: <><path d="M12 2v20"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></>,
    crown: <path d="M2 18h20M3 6l4.5 5L12 4l4.5 7L21 6l-1.5 10H4.5z"/>,
    trophy: <><path d="M6 9a6 6 0 0 0 12 0V3H6z"/><path d="M6 4H4a2 2 0 0 0 0 4h2M18 4h2a2 2 0 0 1 0 4h-2M9 21h6M12 15v6"/></>,
    search: <><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></>,
    filter: <path d="M22 3H2l8 9.46V19l4 2v-8.54z"/>,
    bell: <><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></>,
    settings: <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></>,
    chevron: <path d="m9 18 6-6-6-6"/>,
    chevronDown: <path d="m6 9 6 6 6-6"/>,
    arrowUp: <><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></>,
    arrowDown: <><path d="M12 5v14"/><path d="m19 12-7 7-7-7"/></>,
    sortAsc: <path d="m6 9 6-6 6 6"/>,
    sortDesc: <path d="m6 15 6 6 6-6"/>,
    sortBoth: <><path d="m6 9 6-6 6 6"/><path d="m6 15 6 6 6-6"/></>,
    calendar: <><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></>,
    download: <><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/></>,
    x: <path d="M18 6 6 18M6 6l12 12"/>,
    plus: <path d="M12 5v14M5 12h14"/>,
    dot: <circle cx="12" cy="12" r="10"/>,
    trending: <><path d="m22 7-8.5 8.5-5-5L2 17"/><path d="M16 7h6v6"/></>,
    trendingDown: <><path d="m22 17-8.5-8.5-5 5L2 7"/><path d="M16 17h6v-6"/></>,
    target: <><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></>,
    alert: <><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><path d="M12 9v4M12 17h.01"/></>,
    refresh: <><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/><path d="M3 21v-5h5"/></>,
    layers: <><path d="m12 2 9 5-9 5-9-5 9-5z"/><path d="m3 12 9 5 9-5M3 17l9 5 9-5"/></>,
    shield: <><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/></>,
    list: <><path d="M8 6h13M8 12h13M8 18h13"/><path d="M3 6h.01M3 12h.01M3 18h.01"/></>,
    menu: <path d="M3 6h18M3 12h18M3 18h18"/>,
    globe: <><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20z"/></>,
  };
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill={name==='dot'?color:'none'}
      stroke={name==='dot'?'none':color} strokeWidth={sw} strokeLinecap="round" strokeLinejoin="round" style={style}>
      {P[name]}
    </svg>
  );
};

// ---- Currency-aware money formatters ----
const FX = { code:'USD', rate:1, rates:{ USD:1, BRL:5.38 } };
function applyCurrency(code){ if(FX.rates[code]){ FX.code = code; FX.rate = FX.rates[code]; } }
const _sym = () => FX.code==='BRL' ? 'R$ ' : '$';
const _loc = () => FX.code==='BRL' ? 'pt-BR' : 'en-US';
const USD  = (n) => _sym() + Math.round(n*FX.rate).toLocaleString(_loc());
const USDk = (n) => {
  const v = n*FX.rate, s = _sym();
  return v >= 1e6 ? s + (v/1e6).toLocaleString(_loc(),{maximumFractionDigits:2}) + 'M'
       : v >= 1000 ? s + (v/1000).toLocaleString(_loc(),{maximumFractionDigits:1}) + 'k' : USD(n);
};
// Contagem (carrinhos abandonados etc.) — número puro, respeita o locale da moeda.
const NUM  = (n) => Math.round(n).toLocaleString(_loc());
const NUMk = (n) => n >= 1000 ? (n/1000).toLocaleString(_loc(),{maximumFractionDigits:1}) + 'k' : NUM(n);
const PCT  = (n) => n.toLocaleString('pt-BR',{minimumFractionDigits:1, maximumFractionDigits:1}) + '%';
const SIGN = (n) => (n>=0?'+':'−') + Math.abs(n).toLocaleString('pt-BR',{minimumFractionDigits:1, maximumFractionDigits:1}) + '%';

// Estado de conexão da API p/ o indicador (spec 2026-06-25).
// 'connecting' = 1º load sem resposta ainda; 'live' = último fetch ok; 'offline' = caiu
// (pode ter dado velho na tela ou nenhum). `online` = último fetch deu certo.
function connState({ live, online, hadResult }) {
  if (live && online) return 'live';
  if (!hadResult) return 'connecting';
  return 'offline';
}

// ---- Revenue streams (table columns) ----
const CH = {
  direct:     { key:'direct',     label:'Direto',     color:'#FD7119' },
  logicall:       { key:'logicall',       label:'Logicall',   color:'#D8453B' },
  tauk:       { key:'tauk',       label:'Tauk',       color:'#B03A30' },
  salesbound: { key:'salesbound', label:'Salesbound', color:'#F59E0B' },
  email:      { key:'email',      label:'Email',      color:'#06B6D4' },
  sms:        { key:'sms',        label:'SMS',        color:'#1F9D6B' },
};
const SOURCE_ORDER = ['direct','logicall','tauk','salesbound','email'];

// Plataformas reais do banco
const PLATFORM = {
  buygoods:    { key:'buygoods',    label:'Buygoods',    color:'#FD7119' },
  clickbank:   { key:'clickbank',   label:'Clickbank',   color:'#3B82F6' },
  digistore24: { key:'digistore24', label:'Digistore24', color:'#8B5CF6' },
  pagamerican: { key:'pagamerican', label:'PagAmerican', color:'#1F9D6B' },
};

const initials = (n) => n.replace(/[^A-Za-z]/g,'').slice(0,2).toUpperCase();
const AVATARS = ['#011B36','#303880','#1F9D6B','#8B5CF6','#06B6D4','#E5610E','#16315C','#0A2348'];

function mulberry32(seed){ return function(){ let t = seed += 0x6D2B79F5; t = Math.imul(t ^ t>>>15, t|1); t ^= t + Math.imul(t ^ t>>>7, t|61); return ((t ^ t>>>14)>>>0)/4294967296; }; }

const HANDLES = [
  'LOL_ADS','MIDIA_PRIME','TRAFEGO_X','BLACK_MEDIA','NITRO_ADS','VENDE_MAIS','CONVERTE_BR','PIXEL_KING',
  'ESCALA_PRO','FUNIL_QUENTE','CLICK_MASTER','ROI_HUNTER','ADS_FENIX','TOPO_FUNIL','META_BR','GERA_LEAD',
  'CASH_FLOW','DARK_POST','VIRAL_MIDIA','ALAVANCA','LUCRO_ALTO','CRIATIVO_X','GANHA_BR','PERFORMA',
  'AFILIA_TOP','TRACK_PRO','BOOST_ADS','MAGNETO_MD','ORBITA_ADS','RAPTOR_MID','ZENITH_BR','IMPACTO_X',
  'FLUXO_PRO','PRIME_TRAF','VERTEX_ADS','NOVA_MIDIA','PULSO_ADS','ELITE_TRAF','FORGE_MD','APEX_BR',
  'COMETA_ADS','TITAN_MID','HYPER_BR','SOLAR_ADS','DELTA_TRAF','OMEGA_MD','SIGMA_ADS','LUME_BR',
];

const TARGET_TOTAL = 284320;
const PLATFORM_KEYS = ['buygoods','clickbank','digistore24','pagamerican'];
// Distribuição realista baseada nos dados reais do banco
const PLATFORM_WEIGHTS = [0.75, 0.22, 0.02, 0.01];

// Produtos fictícios p/ modo demo (live usa /api/v1/products)
const PRODUCTS_MOCK = ['Leanzene','Gluconol','Prostavive','Nervovive','Quietum Plus','Sumatra Tonic'];

function buildAffiliates(){
  const rnd = mulberry32(20260601);
  const N = HANDLES.length;
  const raw = HANDLES.map((_,i)=>{
    const w = 1 / Math.pow(i + 3, 0.9);
    return w * (0.86 + rnd()*0.28);
  });
  const wsum = raw.reduce((a,b)=>a+b,0);
  const list = HANDLES.map((name,i)=>{
    let total = raw[i] / wsum * TARGET_TOTAL;
    let rd = 0.50 + (rnd()-0.5)*0.18;
    let rt = 0.20 + (rnd()-0.5)*0.10;
    let rb = 0.15 + (rnd()-0.5)*0.08;
    let re = 0.09 + (rnd()-0.5)*0.05;
    const noSms = rnd() < 0.22;
    let rs = noSms ? 0 : 0.06 + (rnd()-0.5)*0.04;
    const rsum = rd+rt+rb+re+rs;
    rd/=rsum; rt/=rsum; rb/=rsum; re/=rsum; rs/=rsum;
    const direct = total*rd, logicall = total*rt, salesbound = total*rb, email = total*re, sms = total*rs;
    // DOCX 4.1.5 — SMS fora do total até tracking confiável
    total = direct + logicall + salesbound + email;
    let delta;
    const r = rnd();
    if (r < 0.16) delta = -(14 + rnd()*32);
    else if (r < 0.30) delta = -(1 + rnd()*8);
    else delta = 1 + rnd()*22;
    delta = Math.round(delta*10)/10;
    const aov = 78 + rnd()*172;
    const orders = Math.max(1, Math.round(total / aov));
    const aovDelta = Math.round(((rnd()-0.44)*17)*10)/10;
    // FE/BE mock: ~68% das ordens são FE; AOV mock é valor arbitrário de demo (não deriva de
    // receita — mocks representam receita já líquida, sem fator adicional).
    const feOrders = Math.max(1, Math.round(orders * 0.68)), beOrders = orders - feOrders;
    const feRev = direct * 0.66, beRev = direct * 0.34;

    // Plataforma baseada em pesos reais do banco
    const pr = rnd();
    let cumulative = 0, platform = 'buygoods';
    for (let k=0; k<PLATFORM_KEYS.length; k++){
      cumulative += PLATFORM_WEIGHTS[k];
      if (pr < cumulative){ platform = PLATFORM_KEYS[k]; break; }
    }

    const cpaAuthorized = Math.round(direct * (0.40 + rnd()*0.14));
    const overSpend = rnd() < 0.30;
    const cpaPaid = Math.round(cpaAuthorized * (overSpend ? (1.03 + rnd()*0.21) : (0.78 + rnd()*0.19)));
    const cpaOrders = Math.max(1, Math.round(orders * (0.5 + rnd()*0.2)));
    const cpaRateAuth = cpaAuthorized / cpaOrders;
    const cpaRatePaid = cpaPaid / cpaOrders;
    let margin;
    const mr = rnd();
    if (mr < 0.18) margin = -2 + rnd()*5;
    else if (mr < 0.40) margin = 3 + rnd()*2;
    else margin = 5 + rnd()*8.5;
    margin = Math.round(margin*10)/10;
    const cbRate = Math.round((0.3 + rnd()*2.5)*10)/10;
    const refundRate = Math.round((1 + rnd()*6.5)*10)/10;
    const chargeback = total * cbRate/100;
    const refund = total * refundRate/100;
    // Consolidado bruto do canal direto (spec 2026-07-24-consolidado-bruto-global) — reaproveita
    // refundRate/cbRate já existentes no mock, agora aplicados sobre `direct` (não `total`, que
    // inclui gorduras). grossTotal fecha por construção: direct + refundAmountGross + chargebackAmountGross.
    const refundAmountGross = direct * refundRate/100;
    const chargebackAmountGross = direct * cbRate/100;
    const grossTotal = direct + refundAmountGross + chargebackAmountGross;
    const refundCount = Math.round(orders * refundRate/100);
    const chargebackCount = Math.round(orders * cbRate/100);
    const validOrders = orders;
    const since = ['Jan','Fev','Mar','Abr','Mai'][Math.floor(rnd()*5)] + ' 2026';
    const nProd = 1 + Math.floor(rnd()*3);
    const pStart = Math.floor(rnd()*PRODUCTS_MOCK.length);
    const products = Array.from({length:nProd}, (_,k)=>PRODUCTS_MOCK[(pStart+k) % PRODUCTS_MOCK.length]);
    return {
      id:'AFF-' + String(1000 + Math.floor(rnd()*8999)),
      name, traffic: rnd()<0.58 ? 'fb' : 'google', platform,
      direct, logicall, tauk: logicall*0.8, salesbound, logicallRateio: 0, taukRateio: 0, sbRateio: 0, email, sms, total, delta, orders, aov, aovDelta, since,
      feOrders, beOrders, feRev, beRev,
      products,
      cpaAuthorized, cpaPaid, cpaOrders, cpaRateAuth, cpaRatePaid,
      margin, cbRate, refundRate, chargeback, refund,
      grossTotal, refundAmountGross, chargebackAmountGross, refundCount, chargebackCount, validOrders,
      avatar: AVATARS[i % AVATARS.length],
    };
  });
  list.sort((a,b)=>b.total-a.total);
  return list;
}

const AFFILIATES = buildAffiliates();
const ROSTER_TOTAL = 148;
const TOP = AFFILIATES[0];

const MONTHS = ['Jan','Fev','Mar','Abr','Mai','Jun'];
const TREND = (()=> {
  const rnd = mulberry32(424242);
  const end = TARGET_TOTAL;
  const arr = []; let v = end;
  for (let i=MONTHS.length-1; i>=0; i--){ arr[i] = Math.round(v); v = v / (1.07 + (rnd()-0.5)*0.06); }
  return arr;
})();

// ---- API real (FastAPI) ------------------------------------------------
const API_BASE = window.FENIX_API_BASE;  // definida no auth.jsx (carrega antes)

// (extractFamily/groupByFamily removidos em 2026-07-21 — a heurística de prefixo foi
// substituída pela cascata catálogo→keyword de groupProductsByFamilies; eram código morto.)

async function fetchAffiliatesAPI({ period='30d', platform='todas', product='todos', funnel='todos', refundBy='pedido', start, end } = {}) {
  const qs = new URLSearchParams({ period, platform, product, funnel });
  if (refundBy !== 'pedido') qs.set('refund_by', refundBy);
  if (start && end) { qs.set('start', start); qs.set('end', end); }
  const res = await fetch(`${API_BASE}/affiliates?${qs}`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

async function fetchProductsAPI() {
  const res = await fetch(`${API_BASE}/products`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

async function fetchProductFamiliesAPI() {
  const res = await fetch(`${API_BASE}/product-families`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json(); // { families: [{name, variants: [...]}] }
}

function buildFamilyIndex(families) {
  const idx = {};
  for (const {name, variants} of families) {
    for (const v of variants) idx[v] = name;
  }
  return idx;
}

// Agrupamento pela MESMA cascata do filtro/backend (spec 2026-07-21): índice exato do
// catálogo → keyword normalizada (campo `keyword` de /product-families) → nome cru.
// `families` = payload de /product-families (com keyword-only, variants=[]).
function groupProductsByFamilies(productNames, familyIndex, families) {
  const kws = (families || []).filter(f => f.keyword).map(f => [f.keyword, f.name]);
  const grouped = {};
  for (const name of productNames) {
    let fam = familyIndex[name];
    if (!fam) {
      const n = (name || '').toLowerCase().replace(/[^a-z0-9]/g, '');
      const hit = kws.find(([kw]) => n.includes(kw));
      fam = hit ? hit[1] : name;
    }
    (grouped[fam] = grouped[fam] || []).push(name);
  }
  return grouped;
}

async function fetchTrendAPI({ period='30d', platform='todas', product='todos', funnel='todos', start, end } = {}) {
  const qs = new URLSearchParams({ period, platform, product, funnel });
  if (start && end) { qs.set('start', start); qs.set('end', end); }
  const res = await fetch(`${API_BASE}/revenue-trend?${qs}`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// Label do bucket conforme granularidade
function trendLabel(iso, granularity){
  const d = new Date(iso);
  if(granularity==='hour')  return String(d.getHours()).padStart(2,'0')+'h';
  if(granularity==='month') return ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'][d.getMonth()];
  return String(d.getDate()).padStart(2,'0')+'/'+String(d.getMonth()+1).padStart(2,'0');
}

// Série mock por período — espelha shape do /revenue-trend
function buildTrendMock(period, customRange){
  const CFG = {
    ontem:{n:24, gran:'hour'}, '7d':{n:7, gran:'day'}, '14d':{n:14, gran:'day'},
    '30d':{n:30, gran:'day'}, '90d':{n:13, gran:'week'}, ytd:{n:6, gran:'month'},
  };
  let n, gran;
  if(period==='custom' && customRange?.start && customRange?.end){
    const span = Math.max(Math.round((new Date(customRange.end)-new Date(customRange.start))/86400000)+1, 1);
    if(span<=2){ n=24; gran='hour'; } else if(span<=45){ n=span; gran='day'; }
    else if(span<=120){ n=Math.ceil(span/7); gran='week'; } else { n=Math.ceil(span/30); gran='month'; }
  } else {
    ({n, gran} = CFG[period] || CFG['30d']);
  }
  const rnd = mulberry32(99000 + n);
  const periodTotal = TARGET_TOTAL * (PERIOD_FACTOR_MOCK[period] ?? 1);
  const base = periodTotal / n;
  const now = Date.now();
  const stepMs = gran==='hour'?3600e3 : gran==='day'?86400e3 : gran==='week'?7*86400e3 : 30*86400e3;
  return { granularity: gran, points: Array.from({length:n}, (_,i)=>{
    const total = base * (0.72 + rnd()*0.56) * (1 + i/(n*4)); // leve tendência de alta
    const sd=0.50+(rnd()-0.5)*0.1, st=0.20+(rnd()-0.5)*0.06, sb=0.15+(rnd()-0.5)*0.05, se=0.09+(rnd()-0.5)*0.03;
    const ss=Math.max(1-sd-st-sb-se, 0.02);
    const pw=[0.75,0.22,0.02,0.01].map(w=>w*(0.85+rnd()*0.3));
    const pws=pw.reduce((a,b)=>a+b,0);
    return {
      bucket: new Date(now-(n-1-i)*stepMs).toISOString(),
      total,
      bySource:{ direct:total*sd, logicall:total*st, tauk:total*st*0.8, salesbound:total*sb, email:total*se, sms:total*ss },
      byPlatform:{ buygoods:total*pw[0]/pws, clickbank:total*pw[1]/pws, digistore24:total*pw[2]/pws, pagamerican:total*pw[3]/pws },
    };
  })};
}
const PERIOD_FACTOR_MOCK = { ontem:0.033, '7d':0.23, '14d':0.47, '30d':1, '90d':3.0, ytd:5.6, total:9, custom:1 };

async function fetchMetaAPI() {
  const res = await fetch(`${API_BASE}/meta`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// ---- Canais (spec 2026-06-25) ----
const CHANNEL_ORDER = ['direct','logicall','tauk','salesbound','email','sms'];

async function fetchChannelsAPI({ period='ontem', start, end } = {}) {
  const qs = new URLSearchParams({ period });
  if (start && end) { qs.set('start', start); qs.set('end', end); }
  const res = await fetch(`${API_BASE}/channels?${qs}`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// Mock no shape do /channels — só p/ API offline. direct/logicall/salesbound reais; email/sms vazios.
function buildChannelsMock(period='ontem') {
  const factor = PERIOD_FACTOR_MOCK[period] ?? 0.033;
  const BASE = { direct: 80000, logicall: 3500, tauk: 2800, salesbound: 1200 };
  const GORDURA_FRAC = { logicall: 1.0, tauk: 1.0, salesbound: 0.45 };
  const mk = (key, b) => {
    const rnd = mulberry32(key.length * 7919 + 13);
    const series = Array.from({ length: 7 }, (_, i) => {
      const d = new Date(); d.setHours(0,0,0,0); d.setDate(d.getDate() - (6 - i));
      return { bucket: d.toISOString().slice(0,10), value: b * (0.7 + rnd()*0.6) };
    });
    const net  = b * factor * (0.8 + rnd()*0.4);       // vendas válidas (líquido de refund/cgbk)
    const netPrev = net * (0.85 + rnd()*0.3);
    // Consolidado bruto (spec 2026-07-24-consolidado-bruto-global): o kpi do card passa a
    // ser o BRUTO (net já é ~90% do bruto no mock; refund fecha os ~10% restantes).
    const gross = net / 0.9;
    const grossPrev = netPrev / 0.9;
    const refundAmount = gross - net;
    const frac = GORDURA_FRAC[key] ?? 1;
    const out = { key, available: true, series,
             kpi: { value: gross, prevValue: grossPrev,
                    delta: grossPrev ? Math.round((gross-grossPrev)/grossPrev*1000)/10 : null },
             breakdown: { gross, refundAmount, refundCount: Math.round(refundAmount / 235),
                        chargebackAmount: 0, chargebackCount: 0,
                        net, share: key === 'direct' ? net : net * frac,
                        sharePct: key === 'direct' ? 100 : Math.round(frac * 100) } };
    return out;
  };
  return {
    period, seriesWindow: { granularity: 'day' },
    channels: [
      mk('direct', BASE.direct), mk('logicall', BASE.logicall), mk('tauk', BASE.tauk), mk('salesbound', BASE.salesbound),
      { key: 'email', available: false, kpi: null, series: [] },
      { key: 'sms',   available: false, kpi: null, series: [] },
    ],
    generatedAt: null,
  };
}

async function fetchAffiliateDetailAPI({ platform, name, period='30d', product='todos', funnel='todos', refundBy='pedido', start, end } = {}) {
  const qs = new URLSearchParams({ period, product, funnel, ...(start&&end ? {start,end} : {}) });
  if (refundBy !== 'pedido') qs.set('refund_by', refundBy);
  const res = await fetch(
    `${API_BASE}/affiliates/${encodeURIComponent(platform)}/${encodeURIComponent(name)}/detail?${qs}`,
    { signal: AbortSignal.timeout(20000) });
  if (res.status === 404) { const e = new Error('not found'); e.notFound = true; throw e; }
  if (!res.ok) throw new Error('detail HTTP ' + res.status);
  return res.json();
}

// Acompanhamento de Canais — pedidos por plataforma (spec 2026-07-14).
async function fetchAcompanhamento({ source='logicall', period='30d', product='todos', agent='todos', mode='bruto', start, end } = {}) {
  const qs = new URLSearchParams({ source, period, product, agent, mode, ...(start&&end ? {start,end} : {}) });
  const res = await fetch(`${API_BASE}/channels/acompanhamento?${qs}`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error('acompanhamento HTTP ' + res.status);
  return res.json();
}

function acompanhamentoExportUrl({ source='logicall', period='30d', product='todos', agent='todos', mode='bruto', q='', start, end } = {}) {
  const qs = new URLSearchParams({ source, period, product, agent, mode, ...(q ? {q} : {}), ...(start&&end ? {start,end} : {}) });
  return `${API_BASE}/channels/acompanhamento/export?${qs}`;
}

// Detalhe mock — espelha shape do /detail a partir da linha do AFFILIATES (modo demo)
function buildDetailMock(a, period='30d') {
  const buckets = period==='ontem' ? 24 : period==='7d' ? 7 : period==='14d' ? 14
    : period==='90d' ? 13 : period==='ytd' ? 6 : 30;
  const seed = (a.name||'x').length;
  const ticket = a.aov || 180;
  const trend = Array.from({length: buckets}, (_, i) => {
    const sales = Math.max(2, Math.round(8 + 6*Math.sin(i/2 + seed)));
    const refunds = Math.round(sales * (a.refundRate||0) / 100);
    const chargebacks = Math.round(sales * (a.cbRate||0) / 100);
    return { bucket: 'mock-'+i, sales, refunds, chargebacks, revenue: sales * ticket,
      refundAmount: refunds * ticket, chargebackAmount: chargebacks * ticket };
  });
  const available = ['clickbank','digistore24','buygoods'].includes(a.platform);
  const mockRefunds = Math.round((a.orders||0)*(a.refundRate||0)/100);
  const mockCgbk    = Math.round((a.orders||0)*(a.cbRate||0)/100);
  const refundsByFunnel = available ? (a.products || PRODUCTS_MOCK.slice(0,3)).map((p, i) => {
    const sales = Math.max(4, Math.round(40 / (i+1)));
    const refunds = Math.round(sales * (a.refundRate||6) / 100);
    return { product: p, isUpsell: i > 0, sales, refunds, amount: refunds * 180, rate: a.refundRate || 6 };
  }) : null;
  // Upsells sem rastreamento de funil (demo) — ~1-2 itens de outro produto sem principal no dia
  const untrackedUpsells = (seed % 3 === 0)
    ? { count: 2, amount: 392,
        items: [
          { product: 'Glycovit | 3 Bottles | Funnel B3 | Downsell 2', date: 'mock', amount: 196, status: 'Completed' },
          { product: 'Gutbooster | 6 Bottles | Funnel B3 | Upsell 3', date: 'mock', amount: 196, status: 'Completed' },
        ] }
    : { count: 0, amount: 0, items: [] };
  // Top produtos + destrinchamento de funil (demo): cascata decrescente front→upsells→downsell
  const STEP_MOCK = [['Front-end',1],['Upsell 1',0.56],['Upsell 2',0.33],['Downsell 1',0.18]];
  const topProducts = (a.products || PRODUCTS_MOCK).slice(0, 6).map((name, i) => {
    const front = Math.max(8, Math.round(120 / (i+1)));
    const funnel = STEP_MOCK.map(([step, f], j) => ({
      role: j===0 ? 'front' : step.startsWith('Down') ? 'downsell' : 'upsell',
      step, units: Math.round(front * f), pct: Math.round(f * 100),
    }));
    const units = funnel.reduce((s, x) => s + x.units, 0);
    return { name, units, revenue: units * ticket * (1 - i*0.08), funnel };
  });
  // Saúde financeira (demo) — coerente com os mocks de refund
  const refundAmt = a.refund || mockRefunds * 180;
  const cgbkAmt = mockCgbk * 180;
  const vendorNet = (a.direct||0) * 0.35;
  const financialHealth = {
    refundAmount: refundAmt, chargebackAmount: cgbkAmt, chargebackCount: mockCgbk,
    commissionPaid: a.cpaPaid || 0, amountReturned: refundAmt + cgbkAmt,
    netMargin: vendorNet - (available ? refundAmt + cgbkAmt : 0),
  };
  // Coorte por janela + maturação do refund (demo) — spec 2026-07-06
  const _affWins = [['ontem','Ontem',0.02],['7d','−7 dias',0.12],['14d','−14 dias',0.24],['30d','−30 dias',0.45],['90d','−90 dias',1]];
  const cohortWindows = _affWins.map(([key,label,f])=>({ key, label,
    vendas: Math.round((a.orders||0)*f), receita: (a.direct||0)*f,
    refundRate: available ? Math.round((6 + (seed%7)) * 10)/10 : 0 }));
  const _matNorm = [0.03,0.1,0.19,0.36,0.49,0.66,0.77,0.84,0.9,0.945,0.973,0.993,1];
  const _finalRate = available ? (a.refundRate || 9) : 0;
  const refundMaturation = { halfLifeDays: available ? 9 : 0,
    points: Array.from({length:46},(_,day)=>({ day,
      rate: Math.round(_matNorm[Math.min(_matNorm.length-1, Math.floor(day/4))] * _finalRate * 100)/100 })) };
  // Receita bruta (Completed+Refund+CGBK, coorte por venda) — reaproveita os campos já calculados
  // na própria linha (buildAffiliates/apiRowToAffiliate), NÃO recomputa a partir de a.total (que
  // inclui gorduras e duplicava o valor do hero vs. a tabela — spec 2026-07-24).
  const gTotal = a.grossTotal ?? (a.total||0);
  const gRefund = a.refundAmountGross ?? 0;
  const gCgbk = a.chargebackAmountGross ?? 0;
  return {
    affiliate: { name: a.name, affiliateId: a.id, platform: a.platform },
    metrics: { direct: a.direct, orders: a.orders, aov: a.aov, cpaPaid: a.cpaPaid, total: a.total,
      vendorNet, revenuePrev: (a.direct||0) * 0.9, ordersPrev: Math.max(0, (a.orders||0) - 60),
      feRev: (a.direct||0) * 0.64, beRev: (a.direct||0) * 0.36,
      feRevAov: (a.direct||0) * 0.64 * 0.93, beRevAov: (a.direct||0) * 0.36 * 0.93,
      feOrders: Math.round((a.orders||0) * 0.62), beOrders: (a.orders||0) - Math.round((a.orders||0) * 0.62),
      grossTotal: gTotal, grossTotalPrev: gTotal * 0.9,
      refundAmountGross: gRefund, chargebackAmountGross: gCgbk,
      refundCount: a.refundCount ?? mockRefunds, chargebackCount: a.chargebackCount ?? mockCgbk },
    refunds: {
      available,
      direct: available ? {
        refunds: mockRefunds, chargebacks: mockCgbk, cancelamentos: 0,
        rate: a.refundRate||0, amountReturned: a.refund||0,
      } : null,
      logicall: null, salesbound: null,
    },
    financialHealth,
    trend, products: a.products || PRODUCTS_MOCK.slice(0, 3),
    topProducts,
    refundsByFunnel, cohortWindows, refundMaturation, untrackedUpsells,
    activity: { firstSale: '2025-11-12T00:00:00', lastSale: '2026-07-01T00:00:00',
      lifetimeOrders: Math.max((a.orders||0), Math.round((a.orders||0) * 3.2)) },
    granularity: 'day', generatedAt: null,  // null = demo
  };
}

// ---- Página de Produtos (spec 2026-06-30) — escopo buygoods ----
async function fetchProductsOverviewAPI({ period='30d', refundBy='pedido', start, end } = {}) {
  const qs = new URLSearchParams({ period });
  if (refundBy !== 'pedido') qs.set('refund_by', refundBy);
  if (start && end) { qs.set('start', start); qs.set('end', end); }
  const res = await fetch(`${API_BASE}/products/overview?${qs}`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// ---- Nichos (spec 2026-07-07) — buygoods ----
async function fetchNichesAPI({ period='30d', refundBy='pedido', start, end } = {}) {
  const qs = new URLSearchParams({ period });
  if (refundBy !== 'pedido') qs.set('refund_by', refundBy);
  if (start && end) { qs.set('start', start); qs.set('end', end); }
  const res = await fetch(`${API_BASE}/niches?${qs}`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// ---- AOV Tracker (spec 2026-07-13): AOV/CPA por produto por afiliado (submenu de Afiliados) ----
async function fetchAovTrackerAPI({ period='30d', product='todos', q='', start, end } = {}) {
  const qs = new URLSearchParams({ period, product });
  if (q) qs.set('q', q);
  if (start && end) { qs.set('start', start); qs.set('end', end); }
  const res = await fetch(`${API_BASE}/affiliates/aov-tracker?${qs}`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// Mock no shape do /affiliates/aov-tracker — modo demo/offline.
function buildAovTrackerMock(period='30d', product='todos', q='') {
  const factor = PERIOD_FACTOR_MOCK[period] ?? 1;
  const PRODS = ['Jellythin', 'Burntide', 'Leanzene', 'Memo Matrix', 'Vigortrix', 'NerveCell'];
  const NAMES = ['LOL_ADS', 'lucaabreu', 'g15101', 'mediabuyerX', 'traffic_king', 'joaoADS',
    'nova_media', 'peakscale', 'directbuy', 'clickforge', 'zenith_ads', 'bluewhale'];
  let rows = NAMES.map((name, i) => {
    const rnd = mulberry32(name.length * 6151 + i * 97 + 3);
    const revenue = Math.round((320000 - i * 24000) * factor * (0.85 + rnd() * 0.3));
    const nProd = 2 + Math.floor(rnd() * 4);
    const produtos = Array.from({ length: nProd }, (_, k) => {
      const pr = revenue * (k === 0 ? 0.5 : 0.5 / nProd) * (0.7 + rnd() * 0.6);
      const orders = Math.max(1, Math.round(pr / (300 + rnd() * 150)));
      return { name: PRODS[(i + k) % PRODS.length], aov: 300 + rnd() * 180,
               cpaPaid: Math.round(pr * (0.4 + rnd() * 0.15)), orders, revenue: Math.round(pr) };
    }).sort((a, b) => b.revenue - a.revenue);
    const orders = produtos.reduce((s, p) => s + p.orders, 0);
    const cpaPaid = produtos.reduce((s, p) => s + p.cpaPaid, 0);
    return { name, revenue, aov: 320 + rnd() * 150, cpaPaid, orders, produtos };
  }).sort((a, b) => b.revenue - a.revenue);
  const top = rows.slice(0, 10);
  if (q.trim()) {
    const ql = q.trim().toLowerCase();
    const seen = new Set(top.map(r => r.name));
    top.push(...rows.filter(r => r.name.toLowerCase().includes(ql) && !seen.has(r.name)));
  }
  return { rows: top, period, product, generatedAt: null };
}

// ---- EagleLabs C/R (spec 2026-07-09): pedidos cancelados/reembolsados ----
// Janela de período por date_canceled (processamento do estorno). Sem mock offline:
// pedidos individuais inventados seriam dado falso — offline mostra tela vazia.
async function fetchCanceledOrdersAPI({ period='30d', platform='todas', product='todos', funnel='todos', start, end } = {}) {
  const qs = new URLSearchParams({ period, platform, product, funnel });
  if (start && end) { qs.set('start', start); qs.set('end', end); }
  const res = await fetch(`${API_BASE}/orders/canceled?${qs}`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// ---- DRE Afiliados (spec 2026-07-24-dre-afiliados-design): demonstrativo por competência,
// por afiliado ou consolidado ("todos"). SEM refund_by (fixo em pedido) — janela sempre pela
// data do pedido. Todas as contas vêm prontas do backend (inclusive `pct`); zero math aqui. ----
async function fetchDreAPI({ period = '30d', affiliate = 'todos', product = 'todos', funnel = 'todos', start, end } = {}) {
  const qs = new URLSearchParams({ period, affiliate, product, funnel });
  if (start && end) { qs.set('start', start); qs.set('end', end); }
  const res = await fetch(`${API_BASE}/affiliates/dre?${qs}`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// Leads do BuyGoods Bridge (spec 2026-07-27 + adendo UI padrão): Resumo Diário 1:1
// com o painel do bridge, janela pelo period canônico. SEM mock — dado operacional.
async function fetchLeadsAPI({ period, product, start, end } = {}) {
  const params = new URLSearchParams({ period: period || '30d' });
  if (period === 'custom' && start && end) { params.set('start', start); params.set('end', end); }
  if (product && product !== 'todos') params.set('product', product);
  const res = await fetch(`${API_BASE}/leads?${params}`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// Leads & economics por oferta (spec 2026-07-29): página própria em Canais.
// `products` = famílias canônicas separadas por vírgula (seleção múltipla do MultiDropdown);
// vazio = todas, então o param só viaja quando há seleção. SEM mock — dado operacional.
// 30s de timeout: o endpoint cruza banco principal + bridge remoto (4 queries) e, frio,
// leva ~12s em janelas de 90 dias.
async function fetchLeadsEconomicsAPI({ period, start, end, products } = {}) {
  const params = new URLSearchParams({ period: period || '30d' });
  if (period === 'custom' && start && end) { params.set('start', start); params.set('end', end); }
  if (products && products.length) params.set('products', products.join(','));
  const res = await fetch(`${API_BASE}/channels/leads-economics?${params}`, { signal: AbortSignal.timeout(30000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// Teste A/B de funis (spec 2026-07-28): compara duas fatias etapa a etapa. Os filtros
// de cada lado viajam prefixados (ref_/cur_) e só quando diferem do padrão — assim a
// URL fica legível e o cache do backend casa mais. SEM mock: comparação inventada é
// dado falso (mesmo critério do Leads e do EagleLabs).
// ATENÇÃO: `funnel_tag` é o funil de MARKETING (b3/b4…), NÃO o param `funnel` do resto
// do dashboard (que é variação de SKU). São filtros diferentes com nomes parecidos.
async function fetchAbTestAPI({ ref, cur } = {}) {
  const params = new URLSearchParams();
  const put = (pfx, s = {}) => {
    params.set(`${pfx}_period`, s.period || '30d');
    if (s.period === 'custom' && s.customRange) {
      params.set(`${pfx}_start`, s.customRange.start);
      params.set(`${pfx}_end`, s.customRange.end);
    }
    if (s.product && s.product !== 'todos') params.set(`${pfx}_product`, s.product);
    if (s.funnelTag && s.funnelTag !== 'todos') params.set(`${pfx}_funnel_tag`, s.funnelTag);
    if (s.affiliate && s.affiliate !== 'todos') params.set(`${pfx}_affiliate`, s.affiliate);
    if (s.refundBy && s.refundBy !== 'pedido') params.set(`${pfx}_refund_by`, s.refundBy);
  };
  put('ref', ref); put('cur', cur);
  // 30s: são dois scans do banco remoto (~250ms de RTT por round-trip).
  const res = await fetch(`${API_BASE}/products/ab-test?${params}`,
    { signal: AbortSignal.timeout(30000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// Calendário de Reembolso (spec 2026-07-30): compara duas janelas por produto, funil
// de marketing, etapa e motivo. Os filtros de cada lado viajam prefixados (a_/b_) e
// só quando diferem do padrão, para a URL ficar legível e o cache do backend casar
// mais. SEM mock — comparação inventada é dado falso (mesmo critério do Teste A/B).
async function fetchRefundCalendarAPI({ regua = 'pedido', a, b, products, funnelTag } = {}) {
  const params = new URLSearchParams();
  const put = (pfx, s = {}) => {
    params.set(`${pfx}_period`, s.period || '30d');
    if (s.period === 'custom' && s.customRange) {
      params.set(`${pfx}_start`, s.customRange.start);
      params.set(`${pfx}_end`, s.customRange.end);
    }
  };
  put('a', a); put('b', b);
  if (regua !== 'pedido') params.set('regua', regua);
  if (products && products.length) params.set('products', products.join(','));
  if (funnelTag && funnelTag !== 'todos') params.set('funnel_tag', funnelTag);
  // 30s: são 3 scans do banco remoto (~250ms de RTT por round-trip).
  const res = await fetch(`${API_BASE}/refunds/calendar?${params}`,
    { signal: AbortSignal.timeout(30000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// Reembolso Geral (spec 2026-07-31). Params só quando diferem do default (URL limpa +
// cache melhor no backend). 30s: 3 scans do banco remoto.
async function fetchRefundGeneralAPI({ period = '30d', customRange = null, products = [],
  affiliate = 'todos', funnelTag = 'todos', refundBy = 'pedido' } = {}) {
  const params = new URLSearchParams({ period });
  if (period === 'custom' && customRange) {
    params.set('start', customRange.start);
    params.set('end', customRange.end);
  }
  if (products && products.length) params.set('products', products.join(','));
  if (affiliate !== 'todos') params.set('affiliate', affiliate);
  if (funnelTag !== 'todos') params.set('funnel_tag', funnelTag);
  if (refundBy !== 'pedido') params.set('refund_by', refundBy);
  const res = await fetch(`${API_BASE}/refunds/general?${params}`,
    { signal: AbortSignal.timeout(30000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// Mock determinístico (mulberry32) no shape exato do /affiliates/dre — identidades fecham
// POR CONSTRUÇÃO (cada subtotal deriva aritmeticamente dos componentes, igual ao backend):
// RL = RBR − imposto − cgbk − reembolso − projeção; CV = soma dos 5 custos; MC1 = RL − CV;
// marketing.total = soma das 3 comissões; MC2 = MC1 − marketing.total;
// margemSemProjecao = MC2 + projecao.valor. `generatedAt: null` é o selo de dado demo.
// product/funnel entram no seed (não só period/affiliate) — trocar de família/funil no
// filtro precisa mudar os números do mock, senão a tela pareceria travada ao trocar produto.
function buildDreMock(period = '30d', affiliate = 'todos', product = 'todos', funnel = 'todos') {
  const factor = PERIOD_FACTOR_MOCK[period] ?? 1;
  const seedStr = (affiliate || 'todos') + '·' + period + '·' + (product || 'todos') + '·' + (funnel || 'todos');
  const seed = seedStr.split('').reduce((s, c) => s + c.charCodeAt(0), 0) * 97 + seedStr.length * 131 + 11;
  const rnd = mulberry32(seed);

  const rbDireto = 180000 * factor * (0.7 + rnd() * 0.6);

  // Plataformas — Buygoods dominante (padrão real); soma exata por construção (absorve o
  // resto do arredondamento de pesos no maior componente).
  const wBg = 0.90 + rnd() * 0.07;
  const wDs = (1 - wBg) * (0.1 + rnd() * 0.3);
  const wCb = (1 - wBg) * (0.1 + rnd() * 0.3);
  const wPa = Math.max(0, 1 - wBg - wDs - wCb);
  const plataformas = {
    buygoods: rbDireto * wBg, digistore24: rbDireto * wDs,
    clickbank: rbDireto * wCb, pagamerican: rbDireto * wPa,
  };
  const platSum = plataformas.buygoods + plataformas.digistore24 + plataformas.clickbank + plataformas.pagamerican;
  plataformas.buygoods += rbDireto - platSum;

  const taukAtrib = rbDireto * 0.008 * (0.6 + rnd() * 0.7);
  const taukRateio = rbDireto * 0.032 * (0.6 + rnd() * 0.7);
  const sbAtrib = rbDireto * 0.017 * (0.6 + rnd() * 0.7);
  const sbRateio = rbDireto * 0.002 * (0.6 + rnd() * 0.7);
  const recuperacao = {
    tauk: { atribuido: taukAtrib, rateio: taukRateio, total: taukAtrib + taukRateio },
    salesbound: { atribuido: sbAtrib, rateio: sbRateio, total: sbAtrib + sbRateio },
    logicall: { atribuido: 0, rateio: 0, total: 0 },
  };
  const recuperacaoTotal = recuperacao.tauk.total + recuperacao.salesbound.total + recuperacao.logicall.total;

  const receitaBrutaReal = rbDireto + recuperacaoTotal;
  const imposto = 0;

  const cgbkEventos = 1 + Math.floor(rnd() * 3);
  const cgbkValor = rbDireto * (0.002 + rnd() * 0.003);
  const reembolsoN = Math.round(50 + rnd() * 220);
  const reembolsoValor = rbDireto * (0.18 + rnd() * 0.12);

  const realizadoFrac = rbDireto > 0 ? (cgbkValor + reembolsoValor) / rbDireto : null;
  const mediaFrac = realizadoFrac != null ? Math.max(0, realizadoFrac + (rnd() - 0.5) * 0.01) : null;
  const projecaoDisponivel = realizadoFrac != null && mediaFrac != null;
  const projecaoValor = projecaoDisponivel ? (mediaFrac - realizadoFrac) * rbDireto : 0;

  const receitaLiquida = receitaBrutaReal - imposto - cgbkValor - reembolsoValor - projecaoValor;

  // custo de produto e contingência: % sobre a Receita Bruta Real (Direto + recuperação),
  // não sobre RB Direto — decisão do usuário 24/jul, 2ª rodada, alinhada ao financeiro.
  const custoProduto = 7.3 / 100 * receitaBrutaReal;
  const contingencia = 0.2 / 100 * receitaBrutaReal;
  const taxasGateway = rbDireto * (0.05 + rnd() * 0.015);
  const taxasMarketplace = rbDireto * (0.03 + rnd() * 0.015);
  const refuntech = 0;
  const custosTotal = custoProduto + contingencia + taxasGateway + taxasMarketplace + refuntech;

  const margemContribuicao1 = receitaLiquida - custosTotal;

  const comissaoAfiliados = rbDireto * (0.55 + rnd() * 0.12);
  const comissaoRecuperacao = 0;
  const comissaoParceiros = 0;
  const marketingTotal = comissaoAfiliados + comissaoRecuperacao + comissaoParceiros;

  const margemContribuicao2 = margemContribuicao1 - marketingTotal;
  const margemSemProjecao = margemContribuicao2 + projecaoValor;
  const roas = marketingTotal ? receitaBrutaReal / marketingTotal : null;

  const qtdVendas = Math.max(1, Math.round(rbDireto / (280 + rnd() * 90)));
  const aov = qtdVendas ? rbDireto / qtdVendas : 0;

  const pctRb = (v) => (receitaBrutaReal ? Math.round(v / receitaBrutaReal * 10000) / 100 : null);
  const pctRl = (v) => (receitaLiquida ? Math.round(v / receitaLiquida * 10000) / 100 : null);
  const pctRec = (r) => ({ atribuido: pctRb(r.atribuido), rateio: pctRb(r.rateio), total: pctRb(r.total) });

  return {
    periodo: { period, start: null, end: null }, affiliate, product, funnel,
    qtdVendas, aov,
    plataformas, recuperacao,
    receitaBrutaReal, imposto,
    chargeback: { valor: cgbkValor, eventos: cgbkEventos },
    reembolso: { valor: reembolsoValor, n: reembolsoN },
    projecao: {
      valor: projecaoValor,
      mediaPct: mediaFrac != null ? Math.round(mediaFrac * 10000) / 100 : null,
      realizadoPct: realizadoFrac != null ? Math.round(realizadoFrac * 10000) / 100 : null,
      disponivel: projecaoDisponivel,
    },
    receitaLiquida,
    custos: { produto: custoProduto, contingencia, taxasGateway, taxasMarketplace, refuntech, total: custosTotal },
    margemContribuicao1,
    marketing: { comissaoAfiliados, comissaoRecuperacao, comissaoParceiros, total: marketingTotal },
    margemContribuicao2, margemSemProjecao, roas,
    regras: { custoProdutoPct: 7.3, contingenciaPct: 0.2, basePct: 'rb_real' },
    pct: {
      plataformas: { buygoods: pctRb(plataformas.buygoods), digistore24: pctRb(plataformas.digistore24),
        clickbank: pctRb(plataformas.clickbank), pagamerican: pctRb(plataformas.pagamerican) },
      recuperacao: { tauk: pctRec(recuperacao.tauk), salesbound: pctRec(recuperacao.salesbound), logicall: pctRec(recuperacao.logicall) },
      receitaBrutaReal: pctRb(receitaBrutaReal), imposto: pctRb(imposto),
      chargeback: pctRb(cgbkValor), reembolso: pctRb(reembolsoValor),
      projecao: projecaoDisponivel ? pctRb(projecaoValor) : null,
      receitaLiquida: pctRb(receitaLiquida),
      custos: {
        produto: pctRb(custoProduto), contingencia: pctRb(contingencia),
        taxasGateway: pctRb(taxasGateway), taxasMarketplace: pctRb(taxasMarketplace), refuntech: pctRb(refuntech),
        total: { rb: pctRb(custosTotal), rl: pctRl(custosTotal) },
      },
      margemContribuicao1: { rb: pctRb(margemContribuicao1), rl: pctRl(margemContribuicao1) },
      marketing: {
        comissaoAfiliados: pctRb(comissaoAfiliados), comissaoRecuperacao: pctRb(comissaoRecuperacao),
        comissaoParceiros: pctRb(comissaoParceiros),
        total: { rb: pctRb(marketingTotal), rl: pctRl(marketingTotal) },
      },
      margemContribuicao2: { rb: pctRb(margemContribuicao2), rl: pctRl(margemContribuicao2) },
      margemSemProjecao: { rb: pctRb(margemSemProjecao), rl: pctRl(margemSemProjecao) },
    },
    generatedAt: null,
  };
}

// Mock no shape do /niches — modo demo/offline.
function buildNichesMock(period='30d') {
  const factor = PERIOD_FACTOR_MOCK[period] ?? 0.033;
  const BASE = [
    ['ED', 'Erectile Dysfunction', 1580000, [['Horsewood', .66], ['Vigortrix', .32], ['RagnarOak', .02]]],
    ['WL', 'Weight Loss',          1120000, [['Jellythin', .74], ['Burntide', .24], ['MounjaBoost', .02]]],
    ['ML', 'Memory Loss',           590000, [['Memo Matrix', 1]]],
    ['BS', 'Blood Sugar',           216000, [['Glycovit', .66], ['SugarBoost', .34]]],
    ['NP', 'Neuropathy',            140000, [['NerveCell', 1]]],
    ['PT', 'Prostate',               15000, [['Prostabliss', 1]]],
    ['FG', 'Fungus',                  3500, [['MycosynX', 1]]],
    ['VL', 'Vision Loss',             1200, [['Vision X', 1]]],
    ['JT', 'Joint',                    900, [['JoinTrix', 1]]],
    ['OUT','Sem nicho',            1390000, [['Leanzene', .95], ['GutBooster', .03], ['Glucolyn', .02]]],
  ];
  // AOV mock por checkout (spec 2026-07-10); ~68% das ordens são FE, ~66% da receita.
  const _aov = (feRev, feO, beRev, _beO) => (feO ? (feRev + beRev) / feO : 0);
  const mk = (base, share) => {
    const fat = base * factor * share;
    const ped = Math.max(1, Math.round(fat / 250));
    const feO = Math.max(1, Math.round(ped * 0.68)), beO = Math.max(0, ped - feO);
    const feRev = fat * 0.66, beRev = fat * 0.34;
    const feRevAov = feRev * 0.93, beRevAov = beRev * 0.93;
    const rr = Math.round((6 + share * 16) * 10) / 10;
    const refunds = Math.round(ped * rr / 100);
    const chargebacks = Math.round(ped * 0.003);
    // Consolidado bruto (spec 2026-07-24): refund/cgbk em $ pelo ticket médio do produto.
    const avgTicket = ped ? fat / ped : 0;
    const refundAmt = refunds * avgTicket;
    const cgbkAmt = chargebacks * avgTicket;
    return { faturamento: fat, pedidos: ped, feO, beO, feRev, beRev, feRevAov, beRevAov,
      aov: _aov(feRevAov, feO, beRevAov, beO),
      refunds, refundRate: rr,
      chargebacks, cpaPaid: fat * 0.57,
      grossTotal: fat + refundAmt + cgbkAmt, refundAmountGross: refundAmt,
      chargebackAmountGross: cgbkAmt, refundCount: refunds, chargebackCount: chargebacks,
      validOrders: ped };
  };
  const niches = BASE.map(([key, name, base, prods]) => {
    const produtos = prods.map(([pn, sh]) => ({ name: pn, ...mk(base, sh) }));
    const agg = produtos.reduce((a, p) => ({
      faturamento: a.faturamento + p.faturamento, pedidos: a.pedidos + p.pedidos,
      feO: a.feO + p.feO, beO: a.beO + p.beO, feRev: a.feRev + p.feRev, beRev: a.beRev + p.beRev,
      feRevAov: a.feRevAov + p.feRevAov, beRevAov: a.beRevAov + p.beRevAov,
      refunds: a.refunds + p.refunds, chargebacks: a.chargebacks + p.chargebacks,
      cpaPaid: a.cpaPaid + p.cpaPaid,
      refundAmountGross: a.refundAmountGross + p.refundAmountGross,
      chargebackAmountGross: a.chargebackAmountGross + p.chargebackAmountGross }),
      { faturamento: 0, pedidos: 0, feO: 0, beO: 0, feRev: 0, beRev: 0, feRevAov: 0, beRevAov: 0,
        refunds: 0, chargebacks: 0, cpaPaid: 0, refundAmountGross: 0, chargebackAmountGross: 0 });
    const lg = Math.round(agg.faturamento * 0.004);
    return { key, name, ...agg, aov: _aov(agg.feRevAov, agg.feO, agg.beRevAov, agg.beO),
      feOrders: agg.feO, beOrders: agg.beO,
      refundRate: agg.pedidos ? Math.round(agg.refunds / agg.pedidos * 1000) / 10 : 0,
      amountReturned: agg.refunds * 190, produtos,
      logicallGordura: lg, logicallProducts: [{ name: `${name} 6 Bottles Special`, net: lg, orders: 2 }],
      grossTotal: agg.faturamento + agg.refundAmountGross + agg.chargebackAmountGross,
      refundCount: agg.refunds, chargebackCount: agg.chargebacks, validOrders: agg.pedidos };
  }).sort((a, b) => b.faturamento - a.faturamento);
  const tot = niches.reduce((a, n) => ({ f: a.f + n.faturamento, p: a.p + n.pedidos,
    feO: a.feO + n.feO, beO: a.beO + n.beO, feRev: a.feRev + n.feRev, beRev: a.beRev + n.beRev,
    feRevAov: a.feRevAov + n.feRevAov, beRevAov: a.beRevAov + n.beRevAov,
    r: a.r + n.refunds, c: a.c + n.chargebacks, cpa: a.cpa + n.cpaPaid,
    ra: a.ra + n.refundAmountGross, ca: a.ca + n.chargebackAmountGross }),
    { f: 0, p: 0, feO: 0, beO: 0, feRev: 0, beRev: 0, feRevAov: 0, beRevAov: 0, r: 0, c: 0, cpa: 0, ra: 0, ca: 0 });
  return {
    niches,
    kpis: { faturamento: tot.f, pedidos: tot.p, aov: _aov(tot.feRevAov, tot.feO, tot.beRevAov, tot.beO),
      feOrders: tot.feO, beOrders: tot.beO, feRev: tot.feRev, beRev: tot.beRev,
      feRevAov: tot.feRevAov, beRevAov: tot.beRevAov,
      refundRate: tot.p ? Math.round(tot.r / tot.p * 1000) / 10 : 0, chargebacks: tot.c,
      cpaPaid: tot.cpa, activeNiches: niches.filter(n => n.key !== 'OUT' && n.pedidos > 0).length,
      grossTotal: tot.f + tot.ra + tot.ca, refundAmountGross: tot.ra, chargebackAmountGross: tot.ca,
      refundCount: tot.r, chargebackCount: tot.c, validOrders: tot.p },
    global: { logicall: 393000 * factor / 0.033, salesbound: 223000 * factor / 0.033, sms: null,
      logicallSemProduto: 78000 * factor / 0.033, logicallNaoAtribuivel: 236000 * factor / 0.033 },
    period, generatedAt: null,
  };
}

async function fetchProductFunnelAPI({ family, period='30d', funnel='todos', refundBy='pedido', start, end, affiliate } = {}) {
  const qs = new URLSearchParams({ period, funnel });
  if (refundBy !== 'pedido') qs.set('refund_by', refundBy);
  if (start && end) { qs.set('start', start); qs.set('end', end); }
  if (affiliate && affiliate !== 'todos') qs.set('affiliate', affiliate);
  const res = await fetch(`${API_BASE}/products/${encodeURIComponent(family)}/funnel?${qs}`, { signal: AbortSignal.timeout(20000) });
  if (res.status === 404) { const e = new Error('not found'); e.notFound = true; throw e; }
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

async function fetchProductDetailAPI({ family, period='30d', refundBy='pedido', start, end } = {}) {
  const qs = new URLSearchParams({ period });
  if (refundBy !== 'pedido') qs.set('refund_by', refundBy);
  if (start && end) { qs.set('start', start); qs.set('end', end); }
  const res = await fetch(`${API_BASE}/products/${encodeURIComponent(family)}/detail?${qs}`, { signal: AbortSignal.timeout(20000) });
  if (!res.ok) throw new Error(`API ${res.status}`);
  return res.json();
}

// Mock no shape do /products/overview — só p/ API offline (modo demo).
function buildProductsOverviewMock(period='30d') {
  const factor = PERIOD_FACTOR_MOCK[period] ?? 1;
  const fams = ['Leanzene','Jellythin','Horsewood','Memo Matrix','Vigortrix','Glycovit','Burntide','NerveCell'];
  const rnd = mulberry32(7001);
  const rows = fams.map((name,i)=>{
    const gross = (160000/(i+1)) * factor * (0.8+rnd()*0.4);
    const feO = Math.max(1, Math.round(gross/310));
    const beO = Math.round(feO*0.42);
    const operating = gross*0.68;
    const refundAmt = gross*0.12;
    // Consolidado bruto (spec 2026-07-24): cgbk derivado de uma fração pequena do bruto/pedidos.
    const cgbkAmt = gross*0.006;
    const cgbkCount = Math.round((feO+beO)*0.004);
    return { key:name, name, mapped:true, orders:feO+beO, gross,
      aov: feO?(gross*0.93)/feO:0, fe:{orders:feO,rev:gross*0.78}, be:{orders:beO,rev:gross*0.22},
      refunds:{count:Math.round((feO+beO)*0.12), amount:refundAmt},
      refundRate: Math.round((6+rnd()*14)*10)/10, amountReturned:gross*0.14,
      vendorNet:gross*0.4, commission:gross*0.25, operating, maxCpa: feO?operating/feO:0,
      logicallGordura: Math.round(gross*0.004),
      logicallProducts: [{ name:`${name} 6 Bottles Special`, net:Math.round(gross*0.004), orders:2 }],
      grossTotal: gross + refundAmt + cgbkAmt, refundAmountGross: refundAmt,
      chargebackAmountGross: cgbkAmt, refundCount: Math.round((feO+beO)*0.12),
      chargebackCount: cgbkCount, validOrders: feO+beO };
  });
  rows.sort((a,b)=>b.gross-a.gross);
  const totGross=rows.reduce((s,r)=>s+r.gross,0), totFe=rows.reduce((s,r)=>s+r.fe.orders,0);
  const totRefundAmt=rows.reduce((s,r)=>s+r.refundAmountGross,0), totCgbkAmt=rows.reduce((s,r)=>s+r.chargebackAmountGross,0);
  const totRefundCnt=rows.reduce((s,r)=>s+r.refundCount,0), totCgbkCnt=rows.reduce((s,r)=>s+r.chargebackCount,0);
  const totOrders=rows.reduce((s,r)=>s+r.orders,0);
  return { rows,
    kpis:{ revenue:totGross, orders:totOrders, aov: totFe?(totGross*0.93)/totFe:0,
      refundRate:12.5, operating:rows.reduce((s,r)=>s+r.operating,0), activeProducts:rows.length,
      grossTotal: totGross+totRefundAmt+totCgbkAmt, refundAmountGross: totRefundAmt,
      chargebackAmountGross: totCgbkAmt, refundCount: totRefundCnt,
      chargebackCount: totCgbkCnt, validOrders: totOrders },
    logicallSemProduto: 0,
    donut: rows.slice(0,6).map(r=>({name:r.name,value:r.gross}))
      .concat(rows.length>6?[{name:'Outros',value:rows.slice(6).reduce((s,r)=>s+r.gross,0)}]:[]),
    period, generatedAt:null };
}

// Mock no shape do /products/{family}/funnel — modo demo.
function buildFunnelMock(family='Leanzene', period='30d') {
  const costs = { cogs_per_bottle:2.9, ship_fe:9.9, refund_pct:15 };
  const econ = (sp,b,isFe)=> sp - costs.cogs_per_bottle*b - (isFe?costs.ship_fe:0) - sp*costs.refund_pct/100;
  const fe = [1,2,3,6].map(b=>{ const orders=Math.round(70/b)+4, salePrice=89+b*36, rev=orders*salePrice, opp=econ(salePrice,b,true);
    return {label:`FE ${b} bottle${b>1?'s':''}`, bottles:b, orders, rev, salePrice, refunds:Math.round(orders*0.1),
      chargebacks:0, cancelamentos:0, refundRate:10, opp, opProfit:opp*orders, takeRate:null, contribution:opp}; });
  const feOrders = fe.reduce((s,r)=>s+r.orders,0);
  const be = [['Upsell 1',6],['Downsell 1',3]].map(([label,b],i)=>{ const orders=Math.max(1,Math.round(feOrders*(0.3-i*0.16))), salePrice=98+b*30, rev=orders*salePrice, opp=econ(salePrice,b,false);
    return {label, bottles:b, orders, rev, salePrice, refunds:Math.round(orders*0.08), chargebacks:0, cancelamentos:0,
      refundRate:8, opp, opProfit:opp*orders, takeRate: feOrders?orders/feOrders*100:0, contribution: opp*(orders/feOrders)}; });
  const feRev=fe.reduce((s,r)=>s+r.rev,0), beRev=be.reduce((s,r)=>s+r.rev,0);
  const feRevAov=feRev*0.93, beRevAov=beRev*0.93;   // mock buygoods: taxa só no AOV (spec 2026-07-24)
  const opTotal=fe.reduce((s,r)=>s+r.opProfit,0)+be.reduce((s,r)=>s+r.opProfit,0);
  const matrix = { feRows:fe, beRows:be, feOrders, beOrders:be.reduce((s,r)=>s+r.orders,0), feRev, beRev,
    feRevAov, beRevAov,
    summary:{ aov: feOrders?(feRevAov+beRevAov)/feOrders:0, opTotal, feOrders, refundRate:9 } };
  // funnelComplete (mock): funil completo ~ matriz + upsells de outros produtos (~1.35× o BE).
  const beOrders = be.reduce((s,r)=>s+r.orders,0);
  const funnelComplete = { orders: feOrders + Math.round(beOrders*1.35), gross: feRev + beRev*1.35,
    feOrders, beOrders: Math.round(beOrders*1.35), feRev, beRev: beRev*1.35,
    refunds: Math.round((feOrders+beOrders)*0.1), refundRate: 9 };
  return { family, funnels:['B3'], primaryFunnel:'B3', selected:'B3', matrix, funnelComplete,
    compare:[{funnel:'B3', ...matrix.summary}], estimatorDefaults:costs, period, generatedAt:null };
}

// Mock no shape do /products/{family}/detail estendido (spec 2026-07-01) — modo demo.
function buildProductDetailMock(family='Leanzene', period='30d') {
  const factor = PERIOD_FACTOR_MOCK[period] ?? 1;
  const rnd = mulberry32((family||'x').length*131 + 7);
  const revenue = 300000*factor*(0.8+rnd()*0.4);
  const feO = Math.max(1, Math.round(revenue/310));
  const beO = Math.round(feO*0.4);
  const orders = feO+beO;
  const operating = revenue*0.68;
  const stages = [['Front-end',0.58,'fe'],['Upsell 1',0.16,'be'],['Upsell 2',0.09,'be'],['Downsell 1',0.05,'be'],['Back-end',0.12,'be']];
  const funnelByStage = stages.map(([label,f,role])=>({label, rev:revenue*f, pct:Math.round(f*1000)/10, units:Math.round(orders*f), role}));
  const stageUnits = Object.fromEntries(funnelByStage.map(s=>[s.label, s.units]));
  const refundByStage = [['Upsell 2',14.2],['Upsell 1',9.4],['Front-end',6.2],['Downsell 1',5.1],['Back-end',4.4]]
    .map(([label,rate])=>({label, rate, units: stageUnits[label] || Math.round(orders*0.05)}));
  const wins = [['ontem','Ontem',0.01],['7d','7 dias',0.06],['30d','30 dias',0.25],['90d','90 dias',0.6],['total','Todo período',1]];
  const cohortWindows = wins.map(([key,label,f])=>({key,label, vendas:Math.round(orders*f), receita:revenue*f, refundRate:Math.round((6+rnd()*8)*10)/10}));
  const norm = [0.03,0.1,0.19,0.36,0.49,0.66,0.77,0.84,0.9,0.945,0.973,0.993,1];
  const points = Array.from({length:46},(_,day)=>({day, rate: Math.round(norm[Math.min(norm.length-1, Math.floor(day/4))]*7.3*100)/100}));
  const timeseries = Array.from({length:30},(_,i)=>({bucket:'mock-'+i, sales:Math.round(90+30*Math.sin(i/3)+i), refunds:Math.round(6+3*Math.sin(i/2.4))}));
  const AFF = ['Rafael Costa','Marina Souza','Diego Almeida','Camila Ferreira','Lucas Martins'];
  const byAffiliate = AFF.map((name,i)=>({name, affiliateId:'AFF-'+(1000+i), orders:Math.round(orders*(0.24-i*0.04)), revenue:revenue*(0.24-i*0.04)}));
  // Breakdowns de refund (demo) p/ os drawers de etapa e coorte (spec 2026-07-09).
  const _prods = [`${family} | 6 Bottles`, `${family} | 3 Bottles`, `${family} | 9 Bottles | Upsell 1`, `${family} | 2 Bottles | Downsell 1`];
  const mkList = (field, names) => names.slice(0, 3 + Math.floor(rnd()*2)).map((nm,i)=>{
    const refunds = Math.max(1, Math.round((5-i) + rnd()*3));
    return { [field]: nm, refunds, refundAmount: Math.round(refunds*(150 + rnd()*140)) };
  }).sort((a,b)=>b.refundAmount-a.refundAmount);
  refundByStage.forEach(s=>{ s.byProduct = mkList('product', _prods); s.byAffiliate = mkList('name', AFF); });
  cohortWindows.forEach(w=>{ w.byProduct = mkList('product', _prods); w.byAffiliate = mkList('name', AFF);
    w.byStage = mkList('label', ['Front-end','Upsell 1','Upsell 2','Downsell 1']); });
  const pdFeRev = revenue*0.78, pdBeRev = revenue*0.22;   // mock FE/BE split (~fe.rev de buildProductsOverviewMock)
  const pdFeRevAov = pdFeRev*0.93, pdBeRevAov = pdBeRev*0.93; // taxa só no AOV (spec 2026-07-24)
  const pdRefunds = Math.round(orders*0.11), pdCgbk = Math.round(orders*0.005);
  // Consolidado bruto (spec 2026-07-24): refund/cgbk em $ pelo ticket médio do produto.
  const pdAvgTicket = orders ? revenue/orders : 0;
  const pdRefundAmt = pdRefunds*pdAvgTicket, pdCgbkAmt = pdCgbk*pdAvgTicket;
  const pdRevenuePrev = revenue*0.89;
  return { family, metrics:{ revenue, orders, refunds:pdRefunds, chargebacks:pdCgbk,
      aov: feO ? (pdFeRevAov+pdBeRevAov)/feO : 0, operating, margin:operating/revenue*100,
      maxCpa:operating/feO, refundRate:7.3, revenuePrev:pdRevenuePrev, ordersPrev:Math.round(orders*0.9),
      feOrders: feO, beOrders: beO, feRev: pdFeRev, beRev: pdBeRev,
      feRevAov: pdFeRevAov, beRevAov: pdBeRevAov,
      logicallGordura: Math.round(revenue*0.004),
      grossTotal: revenue + pdRefundAmt + pdCgbkAmt, refundAmountGross: pdRefundAmt,
      chargebackAmountGross: pdCgbkAmt, refundCount: pdRefunds, chargebackCount: pdCgbk,
      validOrders: orders, grossTotalPrev: pdRevenuePrev + pdRefundAmt*0.89 + pdCgbkAmt*0.89 },
    logicallProducts: [{ name:`${family} 6 Bottles Special`, net:Math.round(revenue*0.004), orders:2 }],
    funnelByStage, cohortWindows, refundMaturation:{points, halfLifeDays:9}, refundByStage,
    timeseries, byAffiliate, granularity:'day', period, generatedAt:null };
}

// Régua DOCX Seção 5.1 — Refund Rate (compartilhada: página de afiliado e futuros usos)
const refundHealth = (r) =>
  r <= 13.7 ? { color:'var(--positive)', bg:'var(--positive-bg)', label:'Saudável' }
  : r <= 17  ? { color:'var(--warning)',  bg:'var(--warning-bg)',  label:'Atenção' }
  : r <= 20  ? { color:'var(--warning)',  bg:'var(--warning-bg)',  label:'Alerta' }
  : { color:'var(--negative)', bg:'var(--negative-bg)', label:'Corte' };

// Converte linha da API no shape usado pelos componentes (campos mock onde não há fonte)
function apiRowToAffiliate(r, i) {
  return {
    id: r.affiliateId || 'AFF-' + String(9000 + i),
    name: r.name, platform: r.platform,
    traffic: 'fb', // IMPEDIMENTO #1 — CRM Ramon não mapeado; placeholder
    grossTotal: r.grossTotal ?? r.direct ?? 0,
    refundAmountGross: r.refundAmountGross ?? 0, chargebackAmountGross: r.chargebackAmountGross ?? 0,
    refundCount: r.refundCount ?? 0, chargebackCount: r.chargebackCount ?? 0,
    validOrders: r.validOrders ?? r.orders ?? 0,
    direct: r.direct, logicall: r.logicall ?? 0, tauk: r.tauk ?? 0, salesbound: r.salesbound ?? 0,
    logicallRateio: r.logicallRateio ?? 0, taukRateio: r.taukRateio ?? 0, sbRateio: r.sbRateio ?? 0,
    email: r.email ?? 0, sms: r.sms ?? 0, total: r.total,
    orders: r.orders, aov: r.aov, aovDelta: 0, delta: 0, since: '—',
    feOrders: r.feOrders ?? 0, beOrders: r.beOrders ?? 0, feRev: r.feRev ?? 0, beRev: r.beRev ?? 0,
    cpaAuthorized: r.cpaPaid, cpaPaid: r.cpaPaid, cpaOrders: r.orders,
    cpaRateAuth: r.orders ? r.cpaPaid/r.orders : 0,
    cpaRatePaid: r.orders ? r.cpaPaid/r.orders : 0,
    margin: 0, cbRate: 0, refundRate: 0, chargeback: 0, refund: 0,
    directRefunds: r.directRefunds ?? null,
    logicallRefunds:   r.logicallRefunds   ?? null,
    sbRefunds:     r.sbRefunds     ?? null,
    avatar: AVATARS[i % AVATARS.length],
  };
}

// Drawer padrão de deduções do consolidado bruto (spec 2026-07-24-consolidado-bruto-global).
// Arredonda cada componente 1× e deriva a última linha da diferença — as linhas sempre
// fecham na tela, em USD e BRL (mesma técnica do drawer da página de afiliado).
const KDDeducoes = ({ grossTotal, refundAmountGross, refundCount, chargebackAmountGross,
                      chargebackCount, validOrders, partialAmount, partialCount, note }) => {
  const gt = Math.round((grossTotal || 0) * FX.rate);
  const rA = Math.round((refundAmountGross || 0) * FX.rate);
  const cA = Math.round((chargebackAmountGross || 0) * FX.rate);
  const pA = Math.round((partialAmount || 0) * FX.rate);
  const valid = Math.max(0, gt - rA - cA - pA);
  const fmt = (v) => _sym() + v.toLocaleString(_loc());
  const lines = [
    ...(validOrders != null ? [{ label: 'Pedidos válidos', value: NUM(validOrders) }] : []),
    { label: 'Receita total bruta', value: fmt(gt) },
    { label: 'Refunds (' + NUM(refundCount || 0) + ')', value: '− ' + fmt(rA), fg: 'var(--negative)' },
    { label: 'Chargebacks (' + NUM(chargebackCount || 0) + ' eventos)', value: '− ' + fmt(cA), fg: 'var(--negative)' },
    // Parcial devolvido (real): caixa devolvido de refunds PARCIAIS — a venda segue válida
    // (partial refund não muda o status Completed), mas o caixa efetivo é menor. Backend ainda
    // não manda estes campos (2026-08) — render condicional evita linha vazia/quebrada.
    ...((partialAmount || 0) > 0 ? [{ label: 'Parcial devolvido (real, ' + NUM(partialCount || 0) + ' pedidos)',
      value: '− ' + fmt(pA), fg: 'var(--negative)' }] : []),
    { label: 'Vendas válidas (receita bruta)', value: fmt(valid) },
  ];
  return <KDLines eye="Deduções do período" lines={lines}
    note={note || 'Coorte pela data da venda, sem testes, sem frete e sem gorduras.'} />;
};

// Filtro Reembolso (spec 2026-07-22): janela dos devolvidos por data do pedido (padrão,
// coorte de venda) ou data do cancelamento (visão-caixa). Opções compartilhadas por
// FilterBar e filtros locais das páginas de detalhe.
const REFUND_BY_OPTIONS = [
  { key: 'pedido',       label: 'Data do pedido' },
  { key: 'cancelamento', label: 'Data cancelada' },
];

// Nota fixa do modo cancelamento — renderizada em toda tela/seção de refund nesse modo.
const RefundByNote = ({ refundBy }) => refundBy !== 'cancelamento' ? null : (
  <div style={{ font: '600 10.5px/1.4 var(--font-sans)', color: 'var(--fg-3)',
                background: 'var(--gray-100)', borderRadius: 6,
                padding: '6px 10px', marginBottom: 10 }}>
    Reembolsos pela data do cancelamento — o pedido pode ter sido feito dias ou semanas
    antes. Inclui só plataformas com data de cancelamento registrada (Buygoods).
    Taxa = devolvidos processados na janela ÷ vendas da janela.
  </div>
);

Object.assign(window, {
  Icon, USD, USDk, NUM, NUMk, PCT, SIGN, CH, SOURCE_ORDER, PLATFORM, initials,
  AFFILIATES, ROSTER_TOTAL, TOP, MONTHS, TREND, TARGET_TOTAL,
  applyCurrency, FX,
  fetchAffiliatesAPI, fetchProductsAPI, fetchProductFamiliesAPI, fetchTrendAPI, fetchMetaAPI,
  fetchChannelsAPI, buildChannelsMock, CHANNEL_ORDER, connState,
  apiRowToAffiliate, API_BASE,
  fetchAffiliateDetailAPI, buildDetailMock, refundHealth,
  fetchAcompanhamento, acompanhamentoExportUrl,
  PRODUCTS_MOCK, trendLabel, buildTrendMock,
  buildFamilyIndex, groupProductsByFamilies,
  fetchProductsOverviewAPI, fetchProductFunnelAPI, fetchProductDetailAPI,
  buildProductsOverviewMock, buildFunnelMock, buildProductDetailMock,
  fetchNichesAPI, buildNichesMock,
  fetchAovTrackerAPI, buildAovTrackerMock,
  fetchCanceledOrdersAPI,
  KDDeducoes,
  REFUND_BY_OPTIONS, RefundByNote,
  fetchDreAPI, buildDreMock,
  fetchLeadsAPI, fetchLeadsEconomicsAPI, fetchAbTestAPI, fetchRefundCalendarAPI,
  fetchRefundGeneralAPI,
});
