/* global React, Icon, Button, Badge, Card, PageHead, ModalCentral, brl, fmtDate, tmPode, tmMotivoNegado,
          mktRendimentosFaltaTabela, mktEstadosRepasse */

// ─── Marketplace › Rendimentos · Rodada A ─────────────────────────────────────
// A declaração de renda MENSAL da Shopee (PDF do Seller Centre) vira a taxa REAL do
// canal, por loja e mês:
//        taxa = bruto dos lançamentos do app no mês − pagamento total liberado
// É a MESMA conta do `registrarRepasse` (Marketplace.jsx): "Taxa de marketplace" é tudo
// que a plataforma reteve — comissão, taxa de serviço, frete, cupons e descontos —, não
// só a comissão. A tela mostra os quatro pedaços (`rdDecomposicao`), com o reembolso ao
// lado, não dentro.
//
// A conferência (15/09/2026, SUPABASE_marketplace-rendimentos-acumulado.sql): base
// MERCADORIA + frete do comprador; o desvio do MÊS só informa; pede confirmação o ACUMULADO
// do ano fora de ±25%, a partir do 3º mês com venda. Recusa mês com venda na Shopee e zero
// lançamento, mês sem venda na Shopee com lançamento, e mês fora de ordem — o lote grava em
// ordem, loja por loja. Mês sem venda nos dois lados entra como zero, sem linha de taxa.
// O que ela NÃO vê, e a tela diz: buraco menor que ~23% do acumulado.
//
// Quem grava é o banco: `tm_rendimento_mkt_importar` (SUPABASE_marketplace-rendimentos.sql)
// confere tudo de novo numa transação, substitui o mês se pedido, grava a linha em Contas
// a Pagar e a declaração. Esta tela lê o PDF, confere, mostra — e manda o que mostrou.
//
// O que a tela NÃO faz, de propósito:
//  · semanal → Rodada B: lê, mostra que a conta fecha, não grava;
//  · recebível → a declaração resolve a TAXA; o recebível do lançamento continua como
//    está (inadimplência não some por importar declaração — a tela diz isso);
//  · Mercado Livre / TikTok → sem leitor; a taxa deles segue pelo registro de repasse;
//  · rotina → a mensal é RECUPERAÇÃO (2025 e 2026): recusa mês com repasse registrado, e o
//    repasse semanal é a rotina. A rotina de taxa é a semanal amarrada ao lançamento (Rodada B);
//  · venda faltando → a taxa é bruto do app − liberado: com semana faltando no app ela sai
//    menor que a real, ou negativa. O card chama isso de "falta lançar venda", e a ordem da
//    recarga é VENDA ANTES DA DECLARAÇÃO (depois de importar, Lançamentos recusa o período).

// pdf.js: versão FIXADA (nunca @latest), do mesmo CDN do resto do app, carregado só
// quando alguém solta um PDF aqui. Falha de CDN aparece na tela (`pdfjsErro`).
const RD_PDFJS_VERSAO = '3.11.174';
const RD_PDFJS_BASE = 'https://unpkg.com/pdfjs-dist@' + RD_PDFJS_VERSAO;
// % e meses com venda · os MESMOS números de c_faixa_acumulado e c_meses_minimos em
// tm_rendimento_mkt_importar (SUPABASE_marketplace-rendimentos-acumulado.sql). Medidos em
// 15/09/2026 (SUPABASE_diagnostico-desvio-acumulado.sql). Mudou aqui, muda lá.
const RD_FAIXA_ACUMULADO_PCT = 25;
const RD_MESES_MINIMOS_ACUMULADO = 3;
// % do preço do produto: reembolso acima disto ganha aviso ao lado da taxa. Só tela, não trava.
// Medido: 2026 fechou em 4,2% (NG) e 4,9% (Topíssima); os meses ruins da Topíssima em 2025
// foram de 12,4% a 57,5%.
const RD_REEMBOLSO_AVISO_PCT = 10;
// De onde a faixa veio, e o que ela não vê. Decididos em 15/09/2026 — a tela diz os dois.
const RD_FAIXA_ORIGEM = '±25% sobre o acumulado do ano, a partir de 3 meses. Medido em 36 meses de 2025 e nos anos com buraco de 2026: pior ano correto 14,8%, menor buraco 80,2%.';
const RD_FAIXA_LIMITE = 'Esta conferência pega buraco grande — venda faltando a partir de ~23% do acumulado. Buraco menor que isso passa. Ela não substitui conferir semana a semana.';
const RD_CAT_TAXA = 'Taxa de marketplace';
const RD_CHAVE_ESTIMATIVA_2025 = '[ajuste-taxa-mkt-2025|';
const RD_NADA_PRONTO = 'Nenhum arquivo pronto para gravar.';

// As 5 linhas principais do resumo, na ordem em que a Shopee imprime. Com "Preço do
// produto" e "Taxa de frete paga pelo comprador" são as 7 OBRIGATÓRIAS.
// ⚠️ Rótulo casa por IGUALDADE: "taxas" não pode casar com "taxa de frete paga…".
const RD_PAIS = [
  { chave: 'mercadoria', rotulo: 'subtotal de mercadoria', nome: 'Subtotal de mercadoria' },
  { chave: 'cupons', rotulo: 'cupons e rebates', nome: 'Cupons e rebates' },
  { chave: 'frete', rotulo: 'subtotal do frete', nome: 'Subtotal do Frete' },
  { chave: 'taxas', rotulo: 'taxas', nome: 'Taxas' },
  { chave: 'liberado', rotulo: 'pagamento total liberado', nome: 'Pagamento total liberado' },
];
// Sub-linhas que a Shopee já usou (mensal 2025, semanal 2026). Sub-linha é OPCIONAL e a
// Shopee cria novas com o tempo (a "Devolução Fácil" não existia em 2025): linha fora
// desta lista NÃO é recusada nem ignorada — entra na soma do grupo, que tem que fechar,
// e aparece marcada como nova.
const RD_FILHAS_CONHECIDAS = {
  mercadoria: ['preco do produto', 'valor do reembolso', 'cupom'],
  cupons: ['voucher', 'incentivo de cupom', 'incentivo shopee', 'coin cashback'],
  frete: ['taxa de frete paga pelo comprador', 'frete cobrado pelo parceiro logistico', 'desconto de frete pela shopee'],
  taxas: ['taxa de comissao liquida', 'taxa de servico bruta', 'taxa de comissao afiliados do vendedor', 'taxa de devolucao facil shopee'],
};

// ── Texto e números ───────────────────────────────────────────────────────────
function rdNorm(s) {
  return String(s == null ? '' : s).normalize('NFD').replace(/[\u0300-\u036f]/g, '')
    .toLowerCase().replace(/\s+/g, ' ').trim();
}
// "25,863.13" · "-8,395.13" · "R$16,674.03" · "-R$22.68" · "−779.46" (U+2212) → centavos.
// Qualquer outra forma devolve null: número que não sei ler não vira zero.
function rdCentavos(s) {
  const t = String(s == null ? '' : s).trim().replace(/\u2212/g, '-');
  const m = /^(-)?(?:R\$\s?)?(-)?(\d{1,3}(?:,\d{3})+|\d+)\.(\d{2})$/.exec(t);
  if (!m || (m[1] && m[2])) return null;
  const v = Number(m[3].replace(/,/g, '')) * 100 + Number(m[4]);
  return (m[1] || m[2]) ? -v : v;
}
const rdCent = (x) => Math.round((Number(x) || 0) * 100);
const rdFmt = (c) => 'R$ ' + brl((Number(c) || 0) / 100);
const rdPct = (x) => (x == null ? '—' : Math.abs(x).toFixed(1).replace('.', ',') + '%');
const rdPctSinal = (x) => (x == null ? '—' : (x > 0 ? '+' : x < 0 ? '−' : '') + rdPct(x));
const rdMesAno = (iso) => String(iso || '').slice(5, 7) + '/' + String(iso || '').slice(0, 4);
const rdTexto = (e) => String((e && (e.message || e.error_description)) || e || 'erro desconhecido');
const rdDataHora = (ts) => (ts ? new Date(ts).toLocaleString('pt-BR', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : '—');

function rdDataIso(s) {
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(s || ''));
  if (!m) return false;
  const d = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])));
  return d.getUTCFullYear() === Number(m[1]) && d.getUTCMonth() === Number(m[2]) - 1 && d.getUTCDate() === Number(m[3]);
}
function rdFimDoMes(iso) {
  const y = Number(String(iso).slice(0, 4)), m = Number(String(iso).slice(5, 7));
  return String(iso).slice(0, 8) + String(new Date(Date.UTC(y, m, 0)).getUTCDate()).padStart(2, '0');
}
const rdEhMesCheio = (ini, fim) => String(ini).slice(8) === '01' && fim === rdFimDoMes(ini);

// ── Leitura do PDF ────────────────────────────────────────────────────────────
let rdPdfjsPromessa = null;
function rdCarregarPdfjs() {
  if (window.pdfjsLib) return Promise.resolve(window.pdfjsLib);
  if (rdPdfjsPromessa) return rdPdfjsPromessa;
  rdPdfjsPromessa = new Promise((resolve, reject) => {
    const s = document.createElement('script');
    s.src = RD_PDFJS_BASE + '/build/pdf.min.js';
    s.async = true;
    const relogio = setTimeout(() => reject(new Error('o CDN não respondeu em 20 segundos')), 20000);
    s.onload = () => {
      clearTimeout(relogio);
      if (!window.pdfjsLib) { reject(new Error('o arquivo carregou, mas o leitor não se registrou')); return; }
      window.pdfjsLib.GlobalWorkerOptions.workerSrc = RD_PDFJS_BASE + '/build/pdf.worker.min.js';
      resolve(window.pdfjsLib);
    };
    s.onerror = () => { clearTimeout(relogio); reject(new Error('o CDN não entregou o arquivo')); };
    document.head.appendChild(s);
  }).catch(e => { rdPdfjsPromessa = null; throw e; });
  return rdPdfjsPromessa;
}

async function rdTextoDoPdf(lib, buf) {
  const doc = await lib.getDocument({ data: buf, cMapUrl: RD_PDFJS_BASE + '/cmaps/', cMapPacked: true,
    standardFontDataUrl: RD_PDFJS_BASE + '/standard_fonts/' }).promise;
  const paginas = [];
  for (let i = 1; i <= doc.numPages; i++) {
    const pg = await doc.getPage(i);
    const tc = await pg.getTextContent();
    paginas.push(tc.items.map(it => ({ s: it.str, x: it.transform[4], y: it.transform[5] })));
  }
  try { doc.destroy(); } catch (e) { /* só libera o worker */ }
  return paginas;
}

async function rdSha256(buf) {
  if (!window.crypto || !window.crypto.subtle) throw new Error('o navegador não liberou o cálculo do hash (a página precisa estar em https)');
  const h = await window.crypto.subtle.digest('SHA-256', buf);
  return Array.from(new Uint8Array(h)).map(b => b.toString(16).padStart(2, '0')).join('');
}

// Itens do pdf.js → linhas (mesma altura, tolerância 3) → células em ordem de x.
function rdLinhas(paginas) {
  const brutas = [];
  (Array.isArray(paginas) ? paginas : []).forEach((itens, pi) => {
    const vivos = (Array.isArray(itens) ? itens : [])
      .filter(it => it && String(it.s == null ? '' : it.s).trim() !== '')
      .slice().sort((a, b) => (b.y - a.y) || (a.x - b.x));
    let atual = null;
    vivos.forEach(it => {
      if (!atual || Math.abs(atual.y - it.y) > 3) { atual = { pagina: pi + 1, y: it.y, itens: [] }; brutas.push(atual); }
      atual.itens.push(it);
    });
  });
  return brutas.map(l => {
    const cels = l.itens.slice().sort((a, b) => a.x - b.x).map(it => String(it.s).trim());
    const dinheiro = cels.map(rdCentavos);
    const textos = cels.filter((c, i) => dinheiro[i] == null && c !== ':');
    return { pagina: l.pagina, cels, norms: cels.map(rdNorm), valores: dinheiro.filter(v => v != null),
      nome: textos.join(' '), rotulo: rdNorm(textos.join(' ')), texto: cels.join(' | ') };
  });
}

// PURA (testável sem PDF): páginas de itens {s, x, y} → { ok, dados } ou { ok:false, erros }.
// Não guarda nada além de usuário, período e valores: o PDF traz nome do titular e
// conta bancária, e nada disso sai daqui.
function rdLerDeclaracaoShopee(paginas) {
  const erros = [];
  const linhas = rdLinhas(paginas);
  if (!linhas.length) {
    return { ok: false, erros: ['O PDF não tem texto que dê para ler (pode ser imagem escaneada). A declaração do Seller Centre vem com texto: baixe de novo por lá.'] };
  }
  if (!linhas.some(l => l.rotulo === 'declaracao de renda')) {
    erros.push('Não achei o título "Declaração de renda": este PDF não parece a declaração de renda da Shopee.');
  }

  // usuário da Shopee (é o de-para com a loja)
  const lUsu = linhas.filter(l => rdNorm(l.cels.join(' ')).startsWith('nome de usuario'));
  let usuario = '';
  if (lUsu.length !== 1) erros.push(`A linha "Nome de usuário" apareceu ${lUsu.length} vez(es); esperava 1.`);
  else {
    usuario = rdNorm(lUsu[0].cels.join(' ')).replace(/^nome de usuario\s*:?\s*/, '').trim();
    if (!usuario || /\s/.test(usuario)) { erros.push(`Não consegui ler o nome de usuário da Shopee em "${lUsu[0].texto}".`); usuario = ''; }
  }

  // período — na semanal o nome do titular vem na MESMA linha, então casa sem âncora
  const achPer = [];
  linhas.forEach(l => {
    const m = /declaracao de (\d{4}-\d{2}-\d{2}) a (\d{4}-\d{2}-\d{2})/.exec(rdNorm(l.cels.join(' ')));
    if (m) achPer.push(m);
  });
  let inicio = '', fim = '';
  if (achPer.length !== 1) erros.push(`O período ("Declaração de AAAA-MM-DD a AAAA-MM-DD") apareceu ${achPer.length} vez(es); esperava 1.`);
  else if (!rdDataIso(achPer[0][1]) || !rdDataIso(achPer[0][2]) || achPer[0][2] < achPer[0][1]) erros.push(`Período inválido: ${achPer[0][1]} a ${achPer[0][2]}.`);
  else { inicio = achPer[0][1]; fim = achPer[0][2]; }

  // resumo: de "Relatório do pagamento liberado" até o primeiro limite. Sem o limite,
  // "Outros valores referenciais" (que repete "Taxa de serviço bruta") entraria na conta.
  const iIni = linhas.findIndex(l => l.rotulo.startsWith('relatorio do pagamento liberado'));
  const ehFimResumo = (l) => l.rotulo.startsWith('the following is the details') || l.rotulo.startsWith('outros valores referenciais')
    || l.rotulo.startsWith('detalhes do pagamento liberado');
  const iFim = iIni < 0 ? -1 : linhas.findIndex((l, i) => i > iIni && ehFimResumo(l));
  const entradas = [];
  if (iIni < 0) erros.push('Não achei o bloco "Relatório do pagamento liberado".');
  else if (iFim < 0) erros.push('Não achei onde o resumo termina ("Outros valores referenciais" ou "Detalhes do pagamento liberado"). Sem esse limite, linhas de fora da conta seriam lidas como parte dela.');
  else {
    for (let i = iIni + 1; i < iFim; i++) {
      const l = linhas[i];
      if (l.rotulo && l.valores.length === 1) { entradas.push({ rotulo: l.rotulo, nome: l.nome, c: l.valores[0] }); continue; }
      const prox = linhas[i + 1];
      // o valor às vezes cai sozinho na linha de baixo ("Pagamento total liberado" da NG)
      if (l.rotulo && l.valores.length === 0 && i + 1 < iFim && prox && !prox.rotulo && prox.valores.length === 1) {
        entradas.push({ rotulo: l.rotulo, nome: l.nome, c: prox.valores[0] }); i++; continue;
      }
      erros.push(`Linha do resumo que não sei ler: "${l.texto}".`);
    }
  }

  const idx = {};
  let paisOk = iIni >= 0 && iFim >= 0;
  if (paisOk) {
    RD_PAIS.forEach(p => {
      const achados = [];
      entradas.forEach((e, i) => { if (e.rotulo === p.rotulo) achados.push(i); });
      if (achados.length !== 1) { paisOk = false; erros.push(`"${p.nome}" apareceu ${achados.length} vez(es) no resumo; esperava 1.`); }
      else idx[p.chave] = achados[0];
    });
  }
  if (paisOk) {
    const ordem = RD_PAIS.map(p => idx[p.chave]);
    if (ordem.some((v, i) => i > 0 && v <= ordem[i - 1])) {
      paisOk = false;
      erros.push('As linhas principais do resumo vieram fora da ordem de sempre (mercadoria, cupons, frete, taxas, liberado).');
    } else {
      if (idx.mercadoria !== 0) erros.push(`Antes de "Subtotal de mercadoria" o resumo tem linha que não sei encaixar: "${entradas[0].nome}".`);
      if (idx.liberado !== entradas.length - 1) erros.push(`Depois de "Pagamento total liberado" o resumo tem linha que não sei encaixar: "${entradas[idx.liberado + 1].nome}".`);
    }
  }

  const grupos = {};
  const v = {};
  if (paisOk) {
    RD_PAIS.slice(0, 4).forEach((p, k) => {
      const de = idx[p.chave], ate = idx[RD_PAIS[k + 1].chave];
      const filhas = entradas.slice(de + 1, ate).map(e => ({ nome: e.nome, rotulo: e.rotulo, c: e.c,
        nova: !RD_FILHAS_CONHECIDAS[p.chave].some(pref => e.rotulo.startsWith(pref)) }));
      const total = entradas[de].c;
      const soma = filhas.reduce((a, f) => a + f.c, 0);
      if (filhas.length && soma !== total) {
        erros.push(`As sub-linhas de "${p.nome}" somam ${rdFmt(soma)}, mas a linha diz ${rdFmt(total)}: alguma sub-linha não foi lida.`);
      }
      grupos[p.chave] = { nome: p.nome, total, filhas };
      v[p.chave] = total;
    });
    v.liberado = entradas[idx.liberado].c;
    const unica = (grupo, rotulo, nome) => {
      const f = grupos[grupo].filhas.filter(x => x.rotulo === rotulo);
      if (f.length !== 1) {
        erros.push(`"${nome}" apareceu ${f.length} vez(es) dentro de "${grupos[grupo].nome}"; esperava 1. É linha obrigatória: a conferência com o app usa ela.`);
        return null;
      }
      return f[0].c;
    };
    v.preco = unica('mercadoria', 'preco do produto', 'Preço do produto');
    v.freteComprador = unica('frete', 'taxa de frete paga pelo comprador', 'Taxa de frete paga pelo comprador');
    const soma = v.mercadoria + v.cupons + v.frete + v.taxas;
    if (soma !== v.liberado) {
      erros.push(`A conta do resumo não fecha: mercadoria ${rdFmt(v.mercadoria)} + cupons ${rdFmt(v.cupons)} + frete ${rdFmt(v.frete)} + taxas ${rdFmt(v.taxas)} = ${rdFmt(soma)}, mas o liberado é ${rdFmt(v.liberado)}.`);
    }
    if (v.preco != null && v.preco < v.mercadoria) {
      erros.push(`O preço do produto (${rdFmt(v.preco)}) é menor que o subtotal de mercadoria (${rdFmt(v.mercadoria)}). Nos 36 meses de 2025 isso nunca aconteceu: a leitura provavelmente trocou campos.`);
    }
  }

  // detalhes dia a dia: o total tem que ser o resumo, e os dias têm que somar o total
  const semBrl = (n) => n.replace(/\s*\(brl\)$/, '');
  const ehCabecalho = (l) => l.norms[0] === 'pedido completado em' && RD_PAIS.every(p => l.norms.some(n => semBrl(n) === p.rotulo));
  const iCab = linhas.findIndex(ehCabecalho);
  let nDias = 0;
  if (iCab < 0) erros.push('Não achei a tabela "Detalhes do pagamento liberado" (dia a dia). Sem ela não dá para conferir o resumo pelos dias.');
  else {
    const cab = linhas[iCab];
    const col = {};
    RD_PAIS.forEach(p => { col[p.chave] = cab.norms.findIndex(n => semBrl(n) === p.rotulo) - 1; });
    if (cab.norms.length !== 6 || RD_PAIS.some(p => col[p.chave] < 0)) {
      erros.push(`O cabeçalho dos detalhes diários veio diferente do esperado: "${cab.texto}".`);
    } else {
      const mapa = (vals) => { const o = {}; RD_PAIS.forEach(p => { o[p.chave] = vals[col[p.chave]]; }); return o; };
      const igual = (a, b) => RD_PAIS.every(p => a[p.chave] === b[p.chave]);
      const difs = (a, b) => RD_PAIS.filter(p => a[p.chave] !== b[p.chave]).map(p => `${p.nome} ${rdFmt(a[p.chave])} × ${rdFmt(b[p.chave])}`).join('; ');
      const dias = [], totais = [];
      for (let i = iCab + 1; i < linhas.length; i++) {
        const l = linhas[i];
        if (l.rotulo.startsWith('outros detalhes dos valores referenciais') || l.rotulo.startsWith('resumo dos ajustes')
            || l.rotulo.startsWith('detalhes do ajuste')) break;
        if (l.rotulo === 'detalhes do pagamento liberado' || ehCabecalho(l)) continue;
        if (rdDataIso(l.cels[0]) && l.cels.length === 6 && l.valores.length === 5) { dias.push({ data: l.cels[0], ...mapa(l.valores) }); continue; }
        if (l.rotulo === 'pagamento total liberado' && l.valores.length === 5) { totais.push(mapa(l.valores)); continue; }
        erros.push(`Linha dos detalhes diários que não sei ler: "${l.texto}".`);
      }
      nDias = dias.length;
      if (!totais.length) erros.push('Os detalhes diários vieram sem a linha de total ("Pagamento total liberado").');
      else {
        if (totais.some(t => !igual(t, totais[0]))) erros.push('Os detalhes diários trazem totais DIFERENTES entre as páginas.');
        if (paisOk && !igual(totais[0], v)) erros.push(`O total dos detalhes diários não bate com o resumo (detalhe × resumo): ${difs(totais[0], v)}.`);
        const somaDias = {};
        RD_PAIS.forEach(p => { somaDias[p.chave] = dias.reduce((a, d) => a + d[p.chave], 0); });
        if (!igual(somaDias, totais[0])) erros.push(`A soma dos ${dias.length} dias não bate com o total (dias × total): ${difs(somaDias, totais[0])}.`);
      }
      if (inicio) {
        const fora = dias.filter(d => d.data < inicio || d.data > fim).map(d => d.data);
        if (fora.length) erros.push(`Há dia fora do período ${inicio} a ${fim} nos detalhes: ${fora.join(', ')}.`);
      }
    }
  }

  // "Valor total de ajuste" fica FORA do liberado. Aparece 1 a 3 vezes, sempre igual.
  const aj = linhas.filter(l => l.rotulo === 'valor total de ajuste (brl)' && l.valores.length === 1).map(l => l.valores[0]);
  let ajustes = null;
  if (aj.length) {
    if (aj.some(x => x !== aj[0])) erros.push(`"Valor total de ajuste" aparece com valores diferentes: ${aj.map(rdFmt).join(', ')}.`);
    else ajustes = aj[0];
  }

  if (erros.length) return { ok: false, erros };
  return { ok: true, dados: { usuario, inicio, fim, mensal: rdEhMesCheio(inicio, fim), valores: v, ajustes, grupos, dias: nDias } };
}

// ── Contas da tela ────────────────────────────────────────────────────────────
// As QUATRO partes de (bruto do app − liberado), em centavos. Fecham ao centavo POR
// CONSTRUÇÃO: somadas, dão bruto − (mercadoria + cupons + frete + taxas). O reembolso NÃO é
// uma delas: fica ao lado (`rdReembolso`). Dentro, afirmaria que o app tem a venda
// reembolsada — e é justamente isso que não se sabe (decisão de 15/09/2026).
function rdDecomposicao(v, bruto) {
  return [
    { chave: 'taxas', nome: 'Taxas da Shopee', c: -v.taxas,
      dica: 'A linha "Taxas" da declaração: comissão, taxa de serviço, afiliados e o que a Shopee acrescentar.' },
    { chave: 'cuponsFrete', nome: 'Cupons e frete', c: -(v.cupons + v.frete),
      dica: '−(cupons e rebates + subtotal do frete), como a declaração imprime.' },
    { chave: 'freteComprador', nome: 'Frete do comprador', c: v.freteComprador,
      dica: 'O frete que o comprador pagou: está no bruto do app e fica com a Shopee.' },
    { chave: 'appMercadoria', nome: 'App × mercadoria + frete', c: bruto - (v.mercadoria + v.freteComprador),
      dica: 'Bruto do app − (subtotal de mercadoria + frete do comprador). Negativo = o app lançou menos que a Shopee reporta.' },
  ];
}
// A base da conferência: subtotal de mercadoria + frete do comprador (era o preço do produto até
// 15/09/2026). O preço é de antes do reembolso, e o app não tem a venda reembolsada inteira.
const rdBaseShopee = (v) => v.mercadoria + v.freteComprador;
const rdDesvio = (v, bruto) => (bruto > 0 ? (rdBaseShopee(v) - bruto) / bruto * 100 : null);
// "Teve venda na Shopee" = preço do produto + frete do comprador > 0 — a MESMA regra da função.
// É o preço, não a mercadoria: mês todo reembolsado também teve venda.
const rdVendeu = (v) => v.preco + v.freteComprador > 0;
// Reembolso e cupom do vendedor = preço − mercadoria: a mesma conta das medições de 2025 e 2026.
function rdReembolso(v) {
  const c = v.preco - v.mercadoria;
  const pct = v.preco > 0 ? c / v.preco * 100 : null;
  return { c, pct, aviso: pct != null && pct > RD_REEMBOLSO_AVISO_PCT };
}
const rdTextoReembolsoCurto = (re) => rdFmt(re.c) + (re.pct == null ? '' : ` · ${rdPct(re.pct)} do preço`);
const rdTextoReembolso = (re) => `${rdFmt(re.c)} de reembolso neste mês (${rdPct(re.pct)} do preço). Se esses pedidos estão lançados no app, parte desta 'Taxa de marketplace' é devolução, não retenção da plataforma.`;
// O acumulado do ano até o mês, na base mercadoria + frete — a MESMA conta da função. `meses`
// traz o ano em ordem, já com o mês em conferência por último: { shopee, app (centavos), vendeu }.
function rdAcumulado(meses) {
  const lista = Array.isArray(meses) ? meses : [];
  const shopee = lista.reduce((a, m) => a + m.shopee, 0);
  const app = lista.reduce((a, m) => a + m.app, 0);
  const comVenda = lista.filter(m => m.vendeu).length;
  const desvio = app > 0 ? (shopee - app) / app * 100 : null;
  const confere = comVenda >= RD_MESES_MINIMOS_ACUMULADO;
  return { shopee, app, comVenda, desvio, confere,
    fora: confere && (desvio == null || Math.abs(desvio) > RD_FAIXA_ACUMULADO_PCT) };
}
const rdValoresDaLinha = (r) => ({ mercadoria: rdCent(r.subtotal_mercadoria), cupons: rdCent(r.cupons_rebates),
  frete: rdCent(r.subtotal_frete), taxas: rdCent(r.taxas), liberado: rdCent(r.pagamento_liberado),
  preco: rdCent(r.preco_produto), freteComprador: rdCent(r.frete_comprador) });
function rdFaixaObservada(linhas) {
  const ds = (Array.isArray(linhas) ? linhas : []).map(r => rdDesvio(rdValoresDaLinha(r), rdCent(r.bruto_app))).filter(x => x != null);
  return ds.length ? { min: Math.min(...ds), max: Math.max(...ds), n: ds.length } : null;
}

async function rdLerTudo(montar) {
  const out = [];
  for (let de = 0; ; de += 1000) {
    const { data, error } = await montar().range(de, de + 999);
    if (error) return { data: out, error };
    const lote = Array.isArray(data) ? data : [];
    out.push(...lote);
    if (lote.length < 1000) return { data: out, error: null };
  }
}
const rdEhTabelaFaltando = mktRendimentosFaltaTabela;   // a mesma regra do repasse (db.js)
function rdMsgErro(err) {
  if (!err) return '';
  if (err.code === 'PGRST202' || err.code === '42883') return 'O banco não tem a função nesta versão: rode SUPABASE_marketplace-rendimentos.sql e, depois, SUPABASE_marketplace-rendimentos-acumulado.sql.';
  return String(err.message || err) + (err.code ? ` [${err.code}]` : '');
}

function rdMontarBase({ lojas, rend, pedidos, taxas, estimativa, tabelaFalta, comErro }) {
  const L = Array.isArray(lojas) ? lojas : [], R = Array.isArray(rend) ? rend : [];
  const P = Array.isArray(pedidos) ? pedidos : [], T = Array.isArray(taxas) ? taxas : [];
  const lojaById = {}; L.forEach(l => { lojaById[l.id] = l; });
  const lojaDoUsuario = {}, usuarioDaLoja = {};
  R.forEach(r => {
    if (!lojaDoUsuario[r.usuario_plataforma]) lojaDoUsuario[r.usuario_plataforma] = r.loja_id;
    if (!usuarioDaLoja[r.loja_id]) usuarioDaLoja[r.loja_id] = r.usuario_plataforma;
  });
  const pedidosDaLoja = {};
  P.forEach(p => { (pedidosDaLoja[p.marketplace_loja_id] = pedidosDaLoja[p.marketplace_loja_id] || []).push(p); });
  const taxaAmarrada = {}, cpById = {};
  T.forEach(t => { cpById[t.id] = t; if (t.pedido_id) taxaAmarrada[t.pedido_id] = true; });
  return { lojas: L, rend: R, lojaById, lojaDoUsuario, usuarioDaLoja, pedidosDaLoja, taxaAmarrada, cpById,
    estimativa: Number(estimativa) || 0, tabelaFalta: !!tabelaFalta, comErro: !!comErro };
}
function rdBrutoDoPeriodo(base, lojaId, ini, fim) {
  const lista = (base.pedidosDaLoja[lojaId] || []).filter(p => { const d = String(p.data_venda || '').slice(0, 10); return d && d >= ini && d <= fim; });
  return { c: lista.reduce((a, p) => a + rdCent(p.total), 0), n: lista.length,
    amarradas: lista.filter(p => base.taxaAmarrada[p.id]).map(p => p.numero || p.id) };
}
// O mesmo critério do TMR13 no banco: o que tornaria substituir/desfazer um apagão de edição.
function rdLinhaEditada(base, r) {
  if (r.conta_pagar_id == null) return null;   // mês sem venda: não tem linha, de propósito
  const g = base.cpById[r.conta_pagar_id];
  if (!g) return 'a linha de taxa sumiu de Contas a Pagar ou mudou de categoria';
  if (rdCent(g.valor) !== rdCent(r.taxa_gravada)) return `a linha de taxa está com ${rdFmt(rdCent(g.valor))}, e a declaração gravou ${rdFmt(rdCent(r.taxa_gravada))}`;
  if (g.conta_id || g.data_pagamento || g.pedido_id) return 'a linha de taxa ganhou conta, data de pagamento ou vínculo com lançamento';
  return null;
}

// ── A conferência dos arquivos soltos na tela (escopo de módulo, sem React: dá para provar) ──
// Três passos. 1) cada arquivo sozinho: loja, travas, taxa, desvio do mês, reembolso. 2) loja
// por loja, do mês mais antigo ao mais novo — a ordem em que o lote grava —: mês anterior
// faltando e o acumulado do ano. 3) na mesma ordem, quem pode gravar: um mês só grava se o
// anterior que está na lista também gravar.
function rdCompararLote(x, y) {
  const nx = String((x && x.loja && x.loja.nome) || ''), ny = String((y && y.loja && y.loja.nome) || '');
  if (nx !== ny) return nx.localeCompare(ny);
  const ix = String((x && x.loja && x.loja.id) || ''), iy = String((y && y.loja && y.loja.id) || '');
  if (ix !== iy) return ix < iy ? -1 : 1;
  const dx = (x && x.d && x.d.inicio) || '', dy = (y && y.d && y.d.inicio) || '';
  return dx < dy ? -1 : dx > dy ? 1 : 0;
}

function rdAnalisar({ arquivos, base, escolhaLoja, confirmacoes }) {
  const out = {};
  if (!base) return out;
  const escolha = escolhaLoja || {}, confs = confirmacoes || {};
  const vistosSha = {}, vistosPeriodo = {};
  (Array.isArray(arquivos) ? arquivos : []).forEach(a => {
    if (a.estado !== 'lido' || !a.leitura || !a.leitura.ok) return;
    const d = a.leitura.dados, v = d.valores;
    const bloqueios = [];
    if (vistosSha[a.sha]) bloqueios.push(`Arquivo repetido nesta lista: é o mesmo PDF que "${vistosSha[a.sha]}".`);
    else vistosSha[a.sha] = a.nome;
    if (!d.mensal) {
      bloqueios.push(`Declaração semanal (${fmtDate(d.inicio)} a ${fmtDate(d.fim)}). A importação de semanal é a Rodada B e ainda não está liberada: a leitura fechou, mas nada será gravado.`);
    }
    const mapeada = base.lojaDoUsuario[d.usuario] || null;
    const lojaId = mapeada || escolha[d.usuario] || null;
    const loja = lojaId ? (base.lojaById[lojaId] || null) : null;
    const r = { chave: a.chave, d, lojaId, loja, mapeada: !!mapeada, bruto: null, taxa: null, negativa: false,
      desvio: null, fora: false, faixa: null, partes: null, existente: null, bloqueios, decidido: false, pronto: false,
      faltaVenda: false, vendeu: rdVendeu(v), zero: !rdVendeu(v), regra1: false, reembolso: rdReembolso(v),
      acumulado: null, dependeDe: [], depPendente: [] };
    if (lojaId && !loja) bloqueios.push('A loja deste usuário não está mais no cadastro de lojas.');
    if (loja) {
      if (loja.plataforma !== 'Shopee') bloqueios.push(`A loja ${loja.nome} é da plataforma ${loja.plataforma}, e a leitura é da declaração da Shopee.`);
      const dono = base.usuarioDaLoja[loja.id];
      if (!mapeada && dono && dono !== d.usuario) bloqueios.push(`A loja ${loja.nome} já foi importada com o usuário ${dono}, e este arquivo é do usuário ${d.usuario}.`);
      if (!mapeada && Object.keys(escolha).some(u => u !== d.usuario && !base.lojaDoUsuario[u] && escolha[u] === loja.id)) {
        bloqueios.push(`A loja ${loja.nome} foi escolhida também para outro usuário da Shopee nesta lista.`);
      }
      const daLoja = base.rend.filter(x => x.loja_id === loja.id);
      r.existente = daLoja.find(x => x.periodo_inicio === d.inicio && x.periodo_fim === d.fim) || null;
      const sobrepoe = daLoja.filter(x => x !== r.existente && x.periodo_inicio <= d.fim && x.periodo_fim >= d.inicio);
      if (sobrepoe.length) {
        bloqueios.push(`Já existe declaração de outro período que se sobrepõe a este: ${sobrepoe.map(x => fmtDate(x.periodo_inicio) + ' a ' + fmtDate(x.periodo_fim)).join(', ')}.`);
      }
      if (r.existente) {
        const ed = rdLinhaEditada(base, r.existente);
        if (ed) bloqueios.push(`A declaração já importada deste mês não pode ser substituída: ${ed}. Confira a linha em Contas a Pagar antes.`);
      }
      if (d.inicio.slice(0, 4) === '2025' && base.estimativa > 0) {
        bloqueios.push('Existem linhas da ESTIMATIVA de taxa de 2025 em Contas a Pagar: importar a declaração de 2025 dobraria a taxa. Rode antes SUPABASE_taxa-marketplace-2025-estimativa_DESFAZER.sql.');
      }
      const chavePer = loja.id + '|' + d.inicio + '|' + d.fim;
      if (vistosPeriodo[chavePer]) bloqueios.push(`Mesma loja e mesmo período de outro arquivo desta lista ("${vistosPeriodo[chavePer]}").`);
      else vistosPeriodo[chavePer] = a.nome;
      r.bruto = rdBrutoDoPeriodo(base, loja.id, d.inicio, d.fim);
      if (r.bruto.amarradas.length) {
        bloqueios.push(`Lançamentos deste período já têm taxa registrada pelo repasse: ${r.bruto.amarradas.join(', ')}. Importar a declaração contaria a taxa dessas semanas de novo. A declaração mensal é para recuperar meses sem repasse registrado.`);
      }
      // A venda da Shopee contra os lançamentos do mês: as MESMAS recusas da função (TMR16, TMR17).
      if (r.vendeu && r.bruto.n === 0) {
        r.regra1 = true;
        bloqueios.push(`A Shopee vendeu ${rdFmt(v.preco + v.freteComprador)} e o app não tem lançamento nenhum neste mês — lance a venda antes. Não é desvio para confirmar: é ausência.`);
      }
      if (r.zero && r.bruto.n > 0) {
        bloqueios.push(`A declaração não tem venda (preço do produto e frete do comprador zerados), e o app tem ${r.bruto.n} lançamento${r.bruto.n === 1 ? '' : 's'} somando ${rdFmt(r.bruto.c)} neste mês. Loja trocada, mês trocado ou venda que não aconteceu: confira antes.`);
      }
      if (r.zero && (v.mercadoria || v.cupons || v.frete || v.taxas || v.liberado)) {
        bloqueios.push(`A declaração não tem venda, mas traz valores (mercadoria ${rdFmt(v.mercadoria)}, cupons ${rdFmt(v.cupons)}, frete ${rdFmt(v.frete)}, taxas ${rdFmt(v.taxas)}, liberado ${rdFmt(v.liberado)}). Mês sem venda só entra zerado: confira o PDF.`);
      }
      r.taxa = r.bruto.c - v.liberado;
      r.negativa = r.taxa < 0;
      r.desvio = rdDesvio(v, r.bruto.c);
      r.faixa = rdFaixaObservada(daLoja.filter(x => x !== r.existente));
      r.partes = rdDecomposicao(v, r.bruto.c);
    }
    out[a.chave] = r;
  });

  // 2) A ordem do ano e o acumulado, loja por loja.
  const emOrdem = Object.values(out).filter(r => r.loja && r.d.mensal).sort(rdCompararLote);
  emOrdem.forEach(r => {
    const ano = r.d.inicio.slice(0, 4), mes = Number(r.d.inicio.slice(5, 7));
    const meses = [], faltam = [];
    let bloqueadoNaLista = false;
    for (let k = 1; k < mes; k++) {
      const ini = `${ano}-${String(k).padStart(2, '0')}-01`, fim = rdFimDoMes(ini);
      // O da lista vence o do banco: se ele gravar, grava antes deste (substituindo, se for o caso).
      const daLista = emOrdem.find(x => x !== r && x.loja.id === r.loja.id && x.d.inicio === ini && x.d.fim === fim);
      if (daLista && !daLista.bloqueios.length) {
        r.dependeDe.push(daLista);
        meses.push({ shopee: rdBaseShopee(daLista.d.valores), app: daLista.bruto.c, vendeu: daLista.vendeu });
        continue;
      }
      const noBanco = base.rend.find(x => x.loja_id === r.loja.id && x.periodo_inicio === ini && x.periodo_fim === fim);
      if (noBanco) {
        const vb = rdValoresDaLinha(noBanco);
        meses.push({ shopee: rdBaseShopee(vb), app: rdCent(noBanco.bruto_app), vendeu: rdVendeu(vb) });
        continue;
      }
      faltam.push(ini);
      if (daLista) bloqueadoNaLista = true;
    }
    if (faltam.length) {
      r.bloqueios.push(`Falta a declaração de ${faltam.map(rdMesAno).join(', ')} desta loja${bloqueadoNaLista ? ' (a que está nesta lista está bloqueada)' : ''}. A conferência soma o ano em ordem: solte ${faltam.length === 1 ? 'esse mês' : 'esses meses'} junto — mês sem venda também, com o arquivo — ou importe antes.`);
      return;
    }
    meses.push({ shopee: rdBaseShopee(r.d.valores), app: r.bruto.c, vendeu: r.vendeu });
    r.acumulado = rdAcumulado(meses);
    if (r.zero) r.acumulado.fora = false;   // mês zero não soma nada e não confere (a função pula)
    r.fora = r.acumulado.fora;
  });

  // 3) Quem pode gravar, na mesma ordem.
  emOrdem.forEach(r => {
    const cf = confs[r.chave] || {};
    r.faltaVenda = !!(r.regra1 || r.negativa || (r.fora && (r.acumulado.desvio == null || r.acumulado.desvio > 0)));
    r.depPendente = r.dependeDe.filter(x => !x.decidido);
    r.decidido = !!(!r.bloqueios.length && !r.depPendente.length
      && (!r.negativa || cf.negativa) && (!r.fora || cf.acumulado) && (!r.existente || cf.substituir));
    r.pronto = r.decidido && !base.tabelaFalta && !base.comErro;
  });
  Object.values(out).forEach(r => { if (r.loja && !r.d.mensal) r.faltaVenda = !!(r.regra1 || r.negativa); });
  return out;
}

// O acumulado do card: o número, e o que ele quer dizer (linha de baixo no card, junto no modal).
function rdAcumuladoTela(an) {
  if (!an || !an.loja || !an.d || !an.d.mensal) return { valor: '—', nota: '' };
  const ac = an.acumulado;
  if (!ac) return { valor: 'falta mês anterior', nota: 'a conferência soma o ano em ordem' };
  if (an.zero) return { valor: 'não muda', nota: 'mês sem venda não soma nem confere' };
  const valor = ac.desvio == null ? 'sem venda no app' : rdPctSinal(ac.desvio);
  const meses = `${ac.comVenda} ${ac.comVenda === 1 ? 'mês' : 'meses'} com venda`;
  if (!ac.confere) return { valor, nota: `${meses} · confere a partir do ${RD_MESES_MINIMOS_ACUMULADO}º` };
  return { valor, nota: `${meses} · ${ac.fora ? 'fora' : 'dentro'} de ±${RD_FAIXA_ACUMULADO_PCT}%` };
}
const rdTextoAcumulado = (an) => { const t = rdAcumuladoTela(an); return t.nota ? `${t.valor} · ${t.nota}` : t.valor; };

// Por que o card acha que falta lançar venda, numa frase (sem ponto final).
function rdMotivoFaltaVenda(an) {
  const v = an.d.valores;
  if (an.regra1) return `A Shopee vendeu ${rdFmt(v.preco + v.freteComprador)} e o app não tem lançamento nenhum neste mês`;
  if (an.negativa) return `O app tem ${rdFmt(an.bruto.c)} neste mês — menos do que a Shopee liberou (${rdFmt(v.liberado)}), o que não acontece com a venda completa: a Shopee sempre retém parte`;
  const ac = an.acumulado;
  return `No acumulado do ano o app tem ${rdFmt(ac.app)}, ${ac.shopee > 0 ? rdPct(ac.app / ac.shopee * 100) : '—'} do que a Shopee reporta (${rdFmt(ac.shopee)}, mercadoria + frete do comprador, em ${ac.comVenda} meses com venda)`;
}

// ── Pedaços de tela (escopo de módulo: declarados dentro, remontariam a cada render) ──
function RdKV({ rotulo, valor, cls, title }) {
  return (
    <div className={'rd-kv' + (cls ? ' ' + cls : '')} title={title}>
      <span>{rotulo}</span><span className="v">{valor}</span>
    </div>
  );
}

// Caixa de confirmação que responde a QUALQUER jeito de marcar (15/09/2026). O `onChange` do
// React num checkbox vem do evento `click`: quem marca por automação — `checked = true` e um
// evento `change` ou `input`, como faz a ferramenta de formulário dos testes — muda o DOM e o
// React não vê. A caixa aparece marcada e o botão continua desligado; travou três tentativas
// do roteiro contra o banco real. Os ouvintes nativos pegam esses eventos também. O valor é
// sempre o `checked` do elemento, então ouvir o mesmo clique três vezes grava o mesmo valor
// três vezes — nunca inverte.
function RdCaixa({ checked, onMarcar }) {
  const ref = React.useRef(null);
  const cb = React.useRef(onMarcar);
  cb.current = onMarcar;
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return undefined;
    const ouvir = () => cb.current(el.checked);
    el.addEventListener('change', ouvir);
    el.addEventListener('input', ouvir);
    return () => { el.removeEventListener('change', ouvir); el.removeEventListener('input', ouvir); };
  }, []);
  return <input ref={ref} type="checkbox" checked={!!checked} onChange={e => cb.current(e.target.checked)} />;
}

function RdCartao({ a, an, base, lojasShopee, onEscolher, conf, onConf, onTirar }) {
  const leitura = a.leitura;
  const d = leitura && leitura.ok ? leitura.dados : null;
  let selo = null;
  if (a.estado === 'lendo') selo = <Badge tone="neutral">lendo…</Badge>;
  else if (a.estado === 'semlib') selo = <Badge tone="danger">leitor de PDF fora</Badge>;
  else if (a.estado === 'erro' || (leitura && !leitura.ok)) selo = <Badge tone="danger">recusado na leitura</Badge>;
  else if (d && !d.mensal) selo = <Badge tone="neutral">semanal · Rodada B</Badge>;
  else if (an && an.pronto) selo = <Badge tone="success">pronto para gravar</Badge>;
  else if (an && an.decidido) selo = <Badge tone="warning">conferido · gravar indisponível</Badge>;
  else if (an && an.bloqueios.length) selo = <Badge tone="danger">bloqueado</Badge>;
  else if (an && an.faltaVenda) selo = <Badge tone="warning">falta lançar venda?</Badge>;
  else if (an && an.depPendente && an.depPendente.length) selo = <Badge tone="warning">espera {rdMesAno(an.depPendente[0].d.inicio)}</Badge>;
  else if (d) selo = <Badge tone="warning">falta decidir</Badge>;

  const mostraConfirmar = !!(d && an && d.mensal && an.loja && !an.bloqueios.length);

  return (
    <div className="rd-card">
      <div className="rd-card-head">
        <Icon name="file-text" size={16} />
        <div className="rd-card-nome">
          <div className="rd-card-arquivo">{a.nome}</div>
          {d && (
            <div className="rd-card-sub">
              usuário <b>{d.usuario}</b> · {fmtDate(d.inicio)} a {fmtDate(d.fim)} · {d.mensal ? 'mensal' : 'semanal'}
            </div>
          )}
        </div>
        {selo}
        <Button variant="ghost" size="sm" icon="x" onClick={onTirar} title="Tira da lista. Não apaga nada no banco.">Tirar</Button>
      </div>

      {a.estado === 'lendo' && <div className="rd-card-msg">Lendo o PDF…</div>}
      {a.estado === 'semlib' && (
        <div className="rd-card-msg">Não foi lido: o leitor de PDF não carregou (aviso no alto da tela). Nada foi gravado.</div>
      )}
      {a.estado === 'erro' && (
        <div className="rd-card-msg">
          <div className="bal-aviso danger"><Icon name="alert-triangle" size={15} /><div>{a.erro}</div></div>
        </div>
      )}
      {leitura && !leitura.ok && (
        <div className="rd-card-msg">
          <div className="bal-aviso danger">
            <Icon name="alert-triangle" size={15} />
            <div>
              <b>Recusado na leitura — nada foi gravado.</b> A declaração só entra quando todas as travas fecham:
              <ul className="rd-lista">{leitura.erros.map((e, i) => <li key={i}>{e}</li>)}</ul>
            </div>
          </div>
        </div>
      )}

      {d && an && (
        <>
          <div className="rd-card-body">
            <div className={'rd-conferencia' + (an.loja && an.fora ? ' fora' : '')}>
              <div className="rd-bloco-titulo">Conferência · app × Shopee</div>
              <div className="rd-kv">
                <span>Loja</span>
                <span className="v">
                  {an.mapeada && an.loja ? an.loja.nome : (
                    <select className="input rd-select" value={an.lojaId || ''} onChange={e => onEscolher(d.usuario, e.target.value)}>
                      <option value="">— loja do usuário {d.usuario} —</option>
                      {lojasShopee.map(l => {
                        const dono = base.usuarioDaLoja[l.id];
                        return (
                          <option key={l.id} value={l.id} disabled={!!(dono && dono !== d.usuario)}>
                            {l.nome}{dono ? ` (usuário ${dono})` : ''}{l.situacao === 'Arquivada' ? ' · arquivada' : ''}
                          </option>
                        );
                      })}
                    </select>
                  )}
                </span>
              </div>
              {!an.loja ? (
                <div className="rd-nota">
                  Escolha a loja para ver o bruto do app e a taxa. A escolha vale para todos os arquivos do
                  usuário {d.usuario} nesta lista, e fica lembrada depois de gravar.
                </div>
              ) : (
                <>
                  <RdKV rotulo={`Bruto do app no período (${an.bruto.n} lançamento${an.bruto.n === 1 ? '' : 's'})`} valor={rdFmt(an.bruto.c)} />
                  <RdKV rotulo="Shopee: mercadoria + frete do comprador" valor={rdFmt(rdBaseShopee(d.valores))} />
                  <RdKV rotulo="Desvio do mês (só informa)"
                    valor={an.zero ? 'mês sem venda' : an.desvio == null ? 'sem venda no app' : rdPctSinal(an.desvio)} />
                  <RdKV rotulo="Desvio do mês já observado nesta loja"
                    valor={an.faixa ? `${rdPctSinal(an.faixa.min)} a ${rdPctSinal(an.faixa.max)} · ${an.faixa.n} ${an.faixa.n === 1 ? 'mês' : 'meses'}` : 'primeira declaração'} />
                  <RdKV cls={an.fora ? 'alerta' : ''} rotulo="Acumulado do ano (é o que confere)" title={RD_FAIXA_ORIGEM}
                    valor={rdAcumuladoTela(an).valor} />
                  <RdKV cls="sub" rotulo={rdAcumuladoTela(an).nota} valor="" />
                  <RdKV rotulo="Pagamento total liberado" valor={rdFmt(d.valores.liberado)} />
                  <RdKV cls={'total' + (an.negativa ? ' alerta' : '')} rotulo="Taxa a gravar (bruto − liberado)"
                    valor={an.zero ? 'nenhuma · mês sem venda' : rdFmt(an.taxa)} />
                  {an.zero && (
                    <div className="rd-nota">
                      Mês sem venda nos dois lados: entra como zero, sem linha em Contas a Pagar. Conta para a ordem do
                      ano e não soma nada ao acumulado.
                    </div>
                  )}
                  <div className="rd-nota">{RD_FAIXA_ORIGEM} {RD_FAIXA_LIMITE}</div>
                  <div className="rd-nota">
                    A subtração absorve erro de lançamento: se faltar venda no app, a taxa do mês cai no mesmo
                    valor, em silêncio. Por isso esta conferência vem antes de gravar.
                  </div>
                </>
              )}
            </div>

            <div>
              <div className="rd-bloco-titulo">A declaração, lida por rótulo</div>
              {RD_PAIS.slice(0, 4).map(p => {
                const g = d.grupos[p.chave];
                return (
                  <React.Fragment key={p.chave}>
                    <RdKV cls="grupo" rotulo={p.nome} valor={rdFmt(g.total)} />
                    {g.filhas.map((f, i) => (
                      <RdKV key={i} cls="sub" valor={rdFmt(f.c)}
                        rotulo={<>{f.nome}{f.nova && <span className="rd-nova" title="Sub-linha fora das que a Shopee usava. Entrou na soma do grupo, e o grupo fechou.">nova</span>}</>} />
                    ))}
                  </React.Fragment>
                );
              })}
              <RdKV cls="total" rotulo="Pagamento total liberado" valor={rdFmt(d.valores.liberado)} />
              <div className="rd-ok">
                <Icon name="check" size={13} />
                a conta fecha ao centavo, e {d.dias} dia{d.dias === 1 ? '' : 's'} de detalhe somam o resumo
              </div>
              {d.ajustes != null && (
                <div className="rd-nota">
                  Valor total de ajuste: <b>{rdFmt(d.ajustes)}</b>. Fica fora do pagamento liberado e não entra na
                  taxa: é guardado só para conferência. A própria Shopee avisa, na declaração, que esses ajustes
                  “podem ser relacionados a pedidos liberados no passado ou pedidos não liberados”.
                </div>
              )}
            </div>

            <div>
              <div className="rd-bloco-titulo">De onde vem a taxa</div>
              {!an.partes ? (
                <div className="rd-nota">Aparece quando a loja estiver escolhida.</div>
              ) : (
                <>
                  {an.partes.map(p => <RdKV key={p.chave} rotulo={p.nome} valor={rdFmt(p.c)} title={p.dica} />)}
                  <RdKV cls="total" rotulo="Taxa de marketplace" valor={rdFmt(an.taxa)} />
                  <RdKV cls="ao-lado" rotulo="Ao lado, não dentro: reembolso e cupom (preço − mercadoria)"
                    title="Não é uma das partes da taxa: ninguém sabe quanto da venda reembolsada está no bruto do app."
                    valor={rdTextoReembolsoCurto(an.reembolso)} />
                  {an.reembolso.aviso && (
                    <div className="bal-aviso warning">
                      <Icon name="undo-2" size={15} /><div>{rdTextoReembolso(an.reembolso)}</div>
                    </div>
                  )}
                  <div className="rd-nota">
                    É tudo que a plataforma reteve, não só a comissão — a mesma conta do registro de repasse.
                  </div>
                </>
              )}
            </div>
          </div>

          {(a.erroGravar || an.bloqueios.length > 0
            || (mostraConfirmar && (an.negativa || an.fora || an.existente || an.faltaVenda || an.depPendente.length > 0))) && (
            <div className="rd-card-foot">
              {a.erroGravar && (
                <div className="bal-aviso danger">
                  <Icon name="alert-triangle" size={15} />
                  <div><b>O banco recusou — nada foi gravado.</b> {a.erroGravar}</div>
                </div>
              )}
              {an.bloqueios.length > 0 && (
                <div className="bal-aviso danger">
                  <Icon name="lock" size={15} />
                  <div><b>Não dá para gravar:</b><ul className="rd-lista">{an.bloqueios.map((b, i) => <li key={i}>{b}</li>)}</ul></div>
                </div>
              )}
              {mostraConfirmar && an.faltaVenda && (
                <div className="bal-aviso warning">
                  <Icon name="alert-triangle" size={15} />
                  <div>
                    <b>Provável venda faltando no app.</b> {rdMotivoFaltaVenda(an)}.
                    {' '}Isto não é “falta importar”, é <b>falta lançar venda</b>: importar agora grava uma taxa{' '}
                    {an.negativa ? 'negativa' : 'menor que a real'}. Lance o que falta em Marketplace › Lançamentos e
                    importe depois — com a declaração importada, o app não aceita mais lançar venda no período. Se a
                    venda que falta é de um mês já importado, desfaça aquele mês, lance e importe de novo, em ordem.
                  </div>
                </div>
              )}
              {mostraConfirmar && an.depPendente.length > 0 && (
                <div className="rd-nota">
                  Grava depois de {an.depPendente.map(x => rdMesAno(x.d.inicio)).join(', ')}, que {an.depPendente.length === 1 ? 'está' : 'estão'} nesta
                  lista e ainda {an.depPendente.length === 1 ? 'pede' : 'pedem'} decisão: o banco exige os meses do ano em ordem.
                </div>
              )}
              {mostraConfirmar && an.existente && (
                <label className="rd-confirma">
                  <RdCaixa checked={conf.substituir} onMarcar={v => onConf('substituir', v)} />
                  <span>
                    Substituir a declaração deste mês importada em {rdDataHora(an.existente.importado_em)} (taxa{' '}
                    {rdFmt(rdCent(an.existente.taxa_gravada))}, arquivo {an.existente.arquivo_nome})
                    {an.existente.arquivo_sha256 === a.sha ? ' — é o mesmo arquivo.' : '.'} A linha de taxa antiga sai de Contas a Pagar.
                  </span>
                </label>
              )}
              {mostraConfirmar && an.negativa && (
                <label className="rd-confirma">
                  <RdCaixa checked={conf.negativa} onMarcar={v => onConf('negativa', v)} />
                  <span>
                    Gravar mesmo assim, com a taxa <b>NEGATIVA</b> de {rdFmt(an.taxa)}: sei que o app tem menos venda lançada
                    ({rdFmt(an.bruto.c)}) do que a Shopee liberou ({rdFmt(d.valores.liberado)}). Vira despesa negativa no DRE do mês,
                    e o período fica fechado para lançar a venda que falta.
                  </span>
                </label>
              )}
              {mostraConfirmar && an.fora && (
                <label className="rd-confirma">
                  <RdCaixa checked={conf.acumulado} onMarcar={v => onConf('acumulado', v)} />
                  <span>
                    {an.acumulado.desvio == null
                      ? 'Gravar mesmo sem venda do app no acumulado do ano: a conferência não tem com o que comparar.'
                      : <>Gravar mesmo com o acumulado do ano em <b>{rdPctSinal(an.acumulado.desvio)}</b>, fora de
                        ±{RD_FAIXA_ACUMULADO_PCT}%: conferi o app contra a Shopee nos {an.acumulado.comVenda} meses com venda.</>}
                  </span>
                </label>
              )}
            </div>
          )}
        </>
      )}
    </div>
  );
}

// ── Página ────────────────────────────────────────────────────────────────────
function MarketplaceRendimentos({ onNav }) {   // eslint-disable-line no-unused-vars
  const podeCriar = tmPode('marketplace', 'criar');
  const podeExcluir = tmPode('marketplace', 'excluir');
  const [base, setBase] = React.useState(null);            // null = carregando
  const [erroBase, setErroBase] = React.useState(null);
  const [reloadKey, setReloadKey] = React.useState(0);
  const [arquivos, setArquivos] = React.useState([]);
  const [pdfjsErro, setPdfjsErro] = React.useState(null);
  const [escolhaLoja, setEscolhaLoja] = React.useState({}); // usuário da Shopee → loja_id
  const [confirmacoes, setConfirmacoes] = React.useState({});
  const [modalAberto, setModalAberto] = React.useState(false);
  const [conferiu, setConferiu] = React.useState(false);
  const [gravando, setGravando] = React.useState(false);
  const [resultado, setResultado] = React.useState(null);
  const [desfazendo, setDesfazendo] = React.useState(null);
  const [avisoSerie, setAvisoSerie] = React.useState(null);
  const [aberta, setAberta] = React.useState(null);
  const [arrastando, setArrastando] = React.useState(false);
  const inputRef = React.useRef(null);
  const seq = React.useRef(0);

  React.useEffect(() => {
    let vivo = true;
    (async () => {
      if (!window.tmSupabase) { setBase(rdMontarBase({ comErro: true })); setErroBase({ message: 'Sem conexão com o banco.' }); return; }
      setBase(null); setErroBase(null);
      try {
        const sb = window.tmSupabase;
        const [lojasR, rendR, pedR, taxaR, estR] = await Promise.all([
          sb.from('marketplace_lojas').select('id,nome,plataforma,situacao').order('nome'),
          rdLerTudo(() => sb.from('marketplace_rendimentos').select('*').order('id')),
          rdLerTudo(() => sb.from('pedidos').select('id,numero,total,marketplace_loja_id,data_venda')
            .not('marketplace_loja_id', 'is', null).eq('status', 'Atendido').order('id')),
          rdLerTudo(() => sb.from('contas_pagar').select('id,pedido_id,valor,categoria,conta_id,data_pagamento')
            .eq('categoria', RD_CAT_TAXA).order('id')),
          sb.from('contas_pagar').select('id').like('observacoes', '%' + RD_CHAVE_ESTIMATIVA_2025 + '%').limit(1),
        ]);
        if (!vivo) return;
        const tabelaFalta = rdEhTabelaFaltando(rendR.error);
        const erro = [lojasR, pedR, taxaR, estR].map(r => r && r.error).find(Boolean) || (tabelaFalta ? null : rendR.error);
        if (erro) setErroBase(erro);
        setBase(rdMontarBase({ lojas: lojasR.data, rend: tabelaFalta ? [] : rendR.data, pedidos: pedR.data, taxas: taxaR.data,
          estimativa: Array.isArray(estR.data) ? estR.data.length : 0, tabelaFalta, comErro: !!erro }));
      } catch (e) {
        if (vivo) { setErroBase(e); setBase(rdMontarBase({ comErro: true })); }
      }
    })();
    return () => { vivo = false; };
  }, [reloadKey]);

  const lerUm = async (chave, file) => {
    const atualiza = (patch) => setArquivos(arr => arr.map(x => (x.chave === chave ? { ...x, ...patch } : x)));
    if (!(/\.pdf$/i.test(file.name || '') || file.type === 'application/pdf')) {
      atualiza({ estado: 'erro', erro: 'Não é PDF. A declaração de renda do Seller Centre é baixada em PDF.' });
      return;
    }
    let buf, sha;
    try { buf = await file.arrayBuffer(); sha = await rdSha256(buf); }
    catch (e) { atualiza({ estado: 'erro', erro: 'Não consegui ler o arquivo: ' + rdTexto(e) + '. Nada foi lido.' }); return; }
    let lib;
    try { lib = await rdCarregarPdfjs(); }
    catch (e) { setPdfjsErro(rdTexto(e)); atualiza({ estado: 'semlib', sha }); return; }
    setPdfjsErro(null);
    try {
      const paginas = await rdTextoDoPdf(lib, buf);
      atualiza({ estado: 'lido', sha, leitura: rdLerDeclaracaoShopee(paginas), erroGravar: null });
    } catch (e) {
      atualiza({ estado: 'erro', sha, erro: 'Este arquivo não abriu como PDF: ' + rdTexto(e) + '. Nada foi lido.' });
    }
  };

  const adicionar = async (lista) => {
    const files = Array.from(lista || []);
    if (!files.length) return;
    const novos = files.map(f => ({ chave: 'f' + (++seq.current), nome: f.name, file: f, estado: 'lendo' }));
    setArquivos(arr => [...arr, ...novos]);
    setResultado(null);
    for (let i = 0; i < novos.length; i++) await lerUm(novos[i].chave, novos[i].file);
  };

  const tentarDeNovo = async () => {
    setPdfjsErro(null);
    const pend = arquivos.filter(x => x.estado === 'semlib');
    if (!pend.length) {
      try { await rdCarregarPdfjs(); } catch (e) { setPdfjsErro(rdTexto(e)); }
      return;
    }
    for (const x of pend) {
      setArquivos(arr => arr.map(y => (y.chave === x.chave ? { ...y, estado: 'lendo' } : y)));
      await lerUm(x.chave, x.file);
    }
  };

  const tirar = (chave) => {
    setArquivos(arr => arr.filter(x => x.chave !== chave));
    setConfirmacoes(c => { const n = { ...c }; delete n[chave]; return n; });
  };

  const lojasShopee = React.useMemo(() => (base ? base.lojas.filter(l => l.plataforma === 'Shopee') : []), [base]);

  const analise = React.useMemo(() => rdAnalisar({ arquivos, base, escolhaLoja, confirmacoes }),
    [arquivos, base, escolhaLoja, confirmacoes]);

  // Na ordem em que o banco exige gravar (TMR18): loja por loja, do mês mais antigo ao mais novo.
  const prontos = arquivos.filter(a => analise[a.chave] && analise[a.chave].pronto)
    .sort((x, y) => rdCompararLote(analise[x.chave], analise[y.chave]));
  const lidosOk = arquivos.filter(a => a.estado === 'lido' && a.leitura && a.leitura.ok).length;
  const decididos = arquivos.filter(a => analise[a.chave] && analise[a.chave].decidido).length;
  // Os meses que as declarações SOLTAS na tela apontam como venda faltando no app. Sem a
  // declaração o app não sabe o que esperar — por isso a lista vem dos cards, não do banco.
  const faltaVenda = arquivos.map(a => ({ a, an: analise[a.chave] }))
    .filter(x => x.an && x.an.loja && x.an.faltaVenda)
    .map(x => ({ chave: x.a.chave, loja: x.an.loja, an: x.an }));
  const faltaVendaMes = new Set(faltaVenda.map(x => x.loja.id + '|' + x.an.d.inicio.slice(0, 7)));
  const motivoSemGravar = !podeCriar ? tmMotivoNegado('marketplace', 'criar')
    : !base ? 'Carregando os dados do app.'
    : base.tabelaFalta ? 'A tabela das declarações não existe no banco.'
    : base.comErro ? 'Os dados do app não carregaram: sem eles não há conferência.'
    : !prontos.length ? RD_NADA_PRONTO : null;

  const gravar = async () => {
    if (!window.tmSupabase || gravando) return;
    setGravando(true);
    const res = [];
    const recusadas = new Set();   // loja|ano com um mês recusado neste lote: os seguintes nem vão
    for (const a of prontos) {
      const an = analise[a.chave], d = a.leitura.dados, v = d.valores, cf = confirmacoes[a.chave] || {};
      const rotulo = `${an.loja.nome} · ${rdMesAno(d.inicio)}`;
      const lojaAno = an.loja.id + '|' + d.inicio.slice(0, 4);
      if (recusadas.has(lojaAno)) {
        res.push({ chave: a.chave, rotulo, ok: false,
          msg: 'não enviada: um mês anterior desta loja foi recusado neste lote, e o banco exige os meses do ano em ordem. Resolva aquele e grave de novo.' });
        continue;
      }
      try {
        const { data, error } = await window.tmSupabase.rpc('tm_rendimento_mkt_importar', {
          p_loja_id: an.loja.id, p_usuario: d.usuario, p_periodo_inicio: d.inicio, p_periodo_fim: d.fim,
          p_subtotal_mercadoria: v.mercadoria / 100, p_cupons_rebates: v.cupons / 100, p_subtotal_frete: v.frete / 100,
          p_taxas: v.taxas / 100, p_pagamento_liberado: v.liberado / 100, p_preco_produto: v.preco / 100,
          p_frete_comprador: v.freteComprador / 100, p_ajustes_total: d.ajustes == null ? null : d.ajustes / 100,
          p_bruto_app_visto: an.bruto.c / 100, p_arquivo_nome: a.nome, p_arquivo_sha256: a.sha,
          p_substituir: !!(an.existente && cf.substituir), p_confirmar_acumulado: !!(an.fora && cf.acumulado),
          p_confirmar_negativa: !!(an.negativa && cf.negativa),
        });
        if (error) { recusadas.add(lojaAno); res.push({ chave: a.chave, rotulo, ok: false, msg: rdMsgErro(error) }); }
        else res.push({ chave: a.chave, rotulo, ok: true,
          msg: data && data.mes_zero ? 'mês sem venda: gravado como zero, sem linha em Contas a Pagar'
            : `taxa ${rdFmt(rdCent(data && data.taxa))} gravada em Contas a Pagar${data && data.substituiu ? ', no lugar da anterior' : ''}` });
      } catch (e) {
        recusadas.add(lojaAno);
        res.push({ chave: a.chave, rotulo, ok: false, msg: rdTexto(e) });
      }
    }
    const gravados = new Set(res.filter(x => x.ok).map(x => x.chave));
    setArquivos(arr => arr.filter(x => !gravados.has(x.chave))
      .map(x => { const f = res.find(y => y.chave === x.chave); return f ? { ...x, erroGravar: f.msg } : x; }));
    setConfirmacoes(c => { const n = { ...c }; gravados.forEach(k => { delete n[k]; }); return n; });
    setResultado(res);
    setGravando(false);
    setModalAberto(false);
    setConferiu(false);
    setReloadKey(k => k + 1);
  };

  const desfazer = async (linha) => {
    const r = linha.r;
    const nomeLoja = (base.lojaById[r.loja_id] || {}).nome || 'loja';
    // Quantos lançamentos do período têm repasse registrado PELO LÍQUIDO, sem taxa própria
    // (a taxa está nesta declaração): ao desfazer, voltam a pedir conferência. Conta ANTES do confirm.
    const ids = (base.pedidosDaLoja[r.loja_id] || [])
      .filter(p => { const dd = String(p.data_venda || '').slice(0, 10); return dd && dd >= r.periodo_inicio && dd <= r.periodo_fim; })
      .map(p => p.id);
    let voltam = '';
    if (ids.length && window.tmSupabase) {
      const { data: recs, error: recErr } = await window.tmSupabase.from('contas_receber')
        .select('pedido_id,status,valor,valor_recebido').in('pedido_id', ids);
      if (recErr) {
        voltam = '\n\nNão consegui contar os lançamentos com repasse registrado neste período (' + (recErr.message || recErr.code) + '): os que tiverem voltam a pedir conferência.';
      } else {
        const todosDeclarados = {}; ids.forEach(id => { todosDeclarados[id] = true; });
        const est = mktEstadosRepasse(ids, recs, [], todosDeclarados);
        const n = ids.filter(id => est[id] === 'taxa_na_declaracao').length;
        if (n) voltam = `\n\n${n} lançamento${n === 1 ? '' : 's'} deste período ${n === 1 ? 'tem' : 'têm'} repasse registrado pelo líquido, sem taxa própria: ${n === 1 ? 'volta' : 'voltam'} a pedir conferência em Marketplace › Lançamentos, e completar grava a taxa por lançamento.`;
      }
    }
    const zero = r.conta_pagar_id == null;
    const ok = window.confirm(`Desfazer a declaração de ${rdMesAno(r.periodo_inicio)} da ${nomeLoja}?\n\n`
      + (zero ? 'É um mês sem venda: não tem linha de taxa, e o DRE não muda. '
        : `Apaga a declaração e a linha de taxa de ${rdFmt(linha.taxa)} em Contas a Pagar. O DRE daquele mês volta ao que era. `)
      + 'Dá para importar o PDF de novo depois. Os meses seguintes do ano ficam; importar um mês depois deste volta a exigir este.'
      + voltam);
    if (!ok) return;
    setDesfazendo(r.id);
    setAvisoSerie(null);
    const { error } = await window.tmSupabase.rpc('tm_rendimento_mkt_desfazer', { p_rendimento_id: r.id });
    setDesfazendo(null);
    if (error) { setAvisoSerie({ tom: 'danger', msg: `Não desfez ${rdMesAno(r.periodo_inicio)} da ${nomeLoja}: ${rdMsgErro(error)}` }); return; }
    setAvisoSerie({ tom: 'info', msg: zero
      ? `Desfeita a declaração de ${rdMesAno(r.periodo_inicio)} da ${nomeLoja} (mês sem venda, sem linha de taxa).`
      : `Desfeita a declaração de ${rdMesAno(r.periodo_inicio)} da ${nomeLoja}: a linha de taxa de ${rdFmt(linha.taxa)} saiu de Contas a Pagar.` });
    setReloadKey(k => k + 1);
  };

  const serie = React.useMemo(() => {
    if (!base) return [];
    const porLoja = {};
    base.rend.forEach(r => { (porLoja[r.loja_id] = porLoja[r.loja_id] || []).push(r); });
    return Object.keys(porLoja).map(lojaId => {
      const loja = base.lojaById[lojaId] || { nome: '(loja fora do cadastro)' };
      const linhas = porLoja[lojaId].slice().sort((a, b) => (a.periodo_inicio < b.periodo_inicio ? -1 : 1)).map(r => {
        const v = rdValoresDaLinha(r), bruto = rdCent(r.bruto_app), taxa = rdCent(r.taxa_gravada);
        const hoje = rdBrutoDoPeriodo(base, lojaId, r.periodo_inicio, r.periodo_fim);
        return { r, v, bruto, taxa, hoje, baseMudou: hoje.c !== bruto || hoje.n !== Number(r.n_lancamentos),
          editada: rdLinhaEditada(base, r), desvio: rdDesvio(v, bruto), partes: rdDecomposicao(v, bruto),
          zero: r.conta_pagar_id == null, reembolso: rdReembolso(v), acumulado: null, faltaMes: false,
          custoCanal: bruto ? taxa / bruto * 100 : null,
          descontoMerc: v.mercadoria ? (v.mercadoria - v.liberado) / v.mercadoria * 100 : null };
      });
      // O acumulado do ano linha a linha, com a MESMA conta da importação (só mês cheio). Mês
      // desfeito no meio do ano deixa a soma sem ele: a linha diz "falta mês".
      const noAno = {};
      linhas.forEach(l => {
        if (!rdEhMesCheio(l.r.periodo_inicio, l.r.periodo_fim)) return;
        const ano = String(l.r.periodo_inicio).slice(0, 4);
        const meses = noAno[ano] = noAno[ano] || [];
        meses.push({ shopee: rdBaseShopee(l.v), app: l.bruto, vendeu: rdVendeu(l.v) });
        l.acumulado = rdAcumulado(meses);
        if (l.zero) l.acumulado.fora = false;
        l.faltaMes = meses.length !== Number(String(l.r.periodo_inicio).slice(5, 7));
      });
      const anos = [];
      linhas.forEach(l => {
        const ano = String(l.r.periodo_inicio).slice(0, 4);
        let g = anos.find(x => x.ano === ano);
        if (!g) { g = { ano, linhas: [], bruto: 0, liberado: 0, taxa: 0, mercadoria: 0, mercFrete: 0 }; anos.push(g); }
        g.linhas.push(l);
        g.bruto += l.bruto; g.liberado += l.v.liberado; g.taxa += l.taxa; g.mercadoria += l.v.mercadoria;
        g.mercFrete += rdBaseShopee(l.v);
      });
      return { lojaId, loja, anos, faixa: rdFaixaObservada(porLoja[lojaId]), usuario: base.usuarioDaLoja[lojaId] };
    }).sort((a, b) => String(a.loja.nome).localeCompare(String(b.loja.nome)));
  }, [base]);

  const falta = React.useMemo(() => {
    const out = { shopee: [], outras: [] };
    if (!base) return out;
    base.lojas.forEach(l => {
      const peds = base.pedidosDaLoja[l.id] || [];
      if (!peds.length) return;
      if (l.plataforma !== 'Shopee') { out.outras.push({ loja: l, n: peds.length }); return; }
      const meses = {};
      peds.forEach(p => {
        const dia = String(p.data_venda || '').slice(0, 10);
        if (!dia) return;
        const k = dia.slice(0, 7);
        const m = meses[k] = meses[k] || { k, c: 0, n: 0, amarradas: 0 };
        m.c += rdCent(p.total); m.n += 1;
        if (base.taxaAmarrada[p.id]) m.amarradas += 1;
      });
      const daLoja = base.rend.filter(r => r.loja_id === l.id);
      const lista = Object.values(meses).filter(m => {
        const ini = m.k + '-01', fimMes = rdFimDoMes(ini);
        const coberto = daLoja.some(r => r.periodo_inicio <= ini && r.periodo_fim >= fimMes);
        return !coberto && m.amarradas < m.n;
      }).sort((a, b) => (a.k < b.k ? -1 : 1));
      out.shopee.push({ loja: l, lista, meses: Object.keys(meses).length });
    });
    return out;
  }, [base]);

  const soltarArquivos = (e) => {
    e.preventDefault();
    setArrastando(false);
    if (podeCriar) adicionar(e.dataTransfer && e.dataTransfer.files);
  };

  return (
    <div className="canvas">
      <PageHead title="Marketplace · Rendimentos"
        sub="Declaração de renda mensal da Shopee → a taxa real do canal, por loja e mês" />

      <div className="bal-aviso info">
        <Icon name="info" size={15} />
        <div>
          <b>A declaração resolve a TAXA, não o recebível.</b> Importar grava em Contas a Pagar quanto a Shopee
          reteve no mês: o bruto dos lançamentos do app menos o pagamento liberado. Os recebíveis dos lançamentos
          continuam como estão — o que não foi baixado segue em aberto, e a inadimplência não some por importar
          declaração. A linha entra no DRE de Relatórios pelo vencimento (dia 15) e em saldo nenhum: não tem conta
          nem data de pagamento, porque nenhum banco viu o bruto.
        </div>
      </div>

      {/* A regra do repasse em período declarado entrou em 15/09/2026: o repasse baixa o recebível
          pelo líquido e não grava taxa, e o banco recusa taxa em dia declarado (TMR15). */}
      <div className="bal-aviso info">
        <Icon name="rotate-ccw" size={15} />
        <div>
          <b>A declaração mensal é ferramenta de recuperação (2025 e 2026), não de rotina.</b> Mês em que algum
          repasse já foi registrado tem a taxa desses lançamentos e é recusado aqui; como o repasse semanal é a
          rotina, sobram os meses do passado. A rotina da taxa é a declaração semanal amarrada ao lançamento, na
          próxima rodada. Repasse registrado depois, num período com declaração, só baixa o recebível pelo
          líquido: a taxa continua a da declaração, e o banco recusa gravá-la de novo.
        </div>
      </div>

      {erroBase && (
        <div className="bal-aviso danger">
          <Icon name="alert-triangle" size={15} />
          <div><b>Os dados do app não carregaram:</b> {rdTexto(erroBase)}. Sem eles não há conferência, e nada pode ser gravado.</div>
        </div>
      )}
      {base && base.tabelaFalta && (
        <div className="bal-aviso danger">
          <Icon name="alert-triangle" size={15} />
          <div>
            <b>A tabela das declarações ainda não existe no banco.</b> Rode SUPABASE_marketplace-rendimentos.sql e a
            PROVA antes de importar. Dá para ler os PDFs e ver a conferência; gravar, não.
          </div>
        </div>
      )}
      {pdfjsErro && (
        <div className="bal-aviso danger">
          <Icon name="alert-triangle" size={15} />
          <div style={{ flex: 1 }}>
            <b>Não consigo ler PDF agora.</b> O leitor de PDF (pdf.js {RD_PDFJS_VERSAO}, de unpkg.com) não carregou:
            {' '}{pdfjsErro}. Nada foi lido nem gravado. Confira a internet e tente de novo; se continuar, o CDN pode
            estar fora do ar.
          </div>
          <Button variant="secondary" size="sm" icon="refresh-cw" onClick={tentarDeNovo}>Tentar de novo</Button>
        </div>
      )}

      <Card title="Importar declarações" padded>
        <div className={'rd-drop' + (arrastando ? ' ativo' : '') + (podeCriar ? '' : ' desligado')}
          title={podeCriar ? undefined : tmMotivoNegado('marketplace', 'criar')}
          onDragOver={e => { e.preventDefault(); if (podeCriar) setArrastando(true); }}
          onDragLeave={() => setArrastando(false)}
          onDrop={soltarArquivos}
          onClick={() => { if (podeCriar && inputRef.current) inputRef.current.click(); }}>
          <Icon name="file-up" size={22} />
          <div><b>Solte aqui os PDFs da declaração de renda</b> — um ou vários, de lojas e meses diferentes</div>
          <div>ou clique para escolher. Seller Centre › Minha renda › Declaração de renda, mensal.</div>
          {!podeCriar && <div>{tmMotivoNegado('marketplace', 'criar')}</div>}
          <input ref={inputRef} type="file" accept="application/pdf,.pdf" multiple hidden
            onChange={e => { const fs = Array.from(e.target.files || []); e.target.value = ''; adicionar(fs); }} />
        </div>

        {resultado && resultado.length > 0 && (
          <div className={'fc-resultado' + (resultado.some(x => !x.ok) ? ' parcial' : '')} style={{ marginTop: 12, marginBottom: 0 }}>
            <b>{resultado.filter(x => x.ok).length} de {resultado.length} gravada{resultado.length === 1 ? '' : 's'}.</b>
            <ul>
              {resultado.map(x => <li key={x.chave}>{x.rotulo}: {x.ok ? x.msg : <>recusada — {x.msg}</>}</li>)}
            </ul>
          </div>
        )}

        {arquivos.map(a => (
          <RdCartao key={a.chave} a={a} an={analise[a.chave]} base={base || rdMontarBase({})} lojasShopee={lojasShopee}
            conf={confirmacoes[a.chave] || {}}
            onConf={(campo, val) => setConfirmacoes(c => ({ ...c, [a.chave]: { ...(c[a.chave] || {}), [campo]: val } }))}
            onEscolher={(usuario, lojaId) => setEscolhaLoja(m => ({ ...m, [usuario]: lojaId }))}
            onTirar={() => tirar(a.chave)} />
        ))}

        {arquivos.length > 0 && (
          <div className="rd-barra">
            <div className="info">
              {prontos.length} pronto{prontos.length === 1 ? '' : 's'} para gravar
              {lidosOk - decididos > 0 ? ` · ${lidosOk - decididos} com decisão ou bloqueio pendente` : ''}
              {motivoSemGravar && motivoSemGravar !== RD_NADA_PRONTO ? ` · ${motivoSemGravar}` : ''}
            </div>
            <Button variant="primary" size="sm" icon="check" disabled={!!motivoSemGravar}
              title={motivoSemGravar || undefined}
              onClick={() => { setConferiu(false); setModalAberto(true); }}>
              Conferir e gravar {prontos.length || ''}
            </Button>
          </div>
        )}
      </Card>

      <Card title="Série por loja e mês" padded style={{ marginTop: 16 }}>
        {avisoSerie && (
          <div className={'bal-aviso ' + avisoSerie.tom}>
            <Icon name={avisoSerie.tom === 'danger' ? 'alert-triangle' : 'info'} size={15} /><div>{avisoSerie.msg}</div>
          </div>
        )}
        {!base ? (
          <div className="rd-nota">Carregando…</div>
        ) : serie.length === 0 ? (
          <div className="rd-nota">Nenhuma declaração importada ainda.</div>
        ) : serie.map(s => (
          <div key={s.lojaId}>
            <div className="rd-loja-head">
              <div className="rd-loja-nome">{s.loja.nome} <span className="rd-loja-meta">· usuário {s.usuario}</span></div>
              <div className="rd-loja-meta">
                Desvio do mês observado (só informa): {s.faixa ? `${rdPctSinal(s.faixa.min)} a ${rdPctSinal(s.faixa.max)} em ${s.faixa.n} ${s.faixa.n === 1 ? 'mês' : 'meses'}` : '—'}
                {' '}· confere o acumulado do ano: ±{RD_FAIXA_ACUMULADO_PCT}%, a partir do {RD_MESES_MINIMOS_ACUMULADO}º mês com venda
              </div>
            </div>
            <div className="rd-tbl-wrap">
              <table className="tbl rd-tbl">
                <thead>
                  <tr>
                    <th>Mês</th>
                    <th className="n">Bruto do app</th>
                    <th className="n">Liberado</th>
                    <th className="n">Taxa gravada</th>
                    <th className="n" title="Taxa gravada ÷ bruto do app. É o número que vai para o DRE.">Custo total do canal</th>
                    <th className="n" title="(Subtotal de mercadoria − pagamento liberado) ÷ subtotal de mercadoria. Só números da Shopee.">Desconto da Shopee sobre a mercadoria</th>
                    <th className="n" title="(Subtotal de mercadoria + frete do comprador − bruto do app) ÷ bruto do app. Só informa.">Desvio do mês</th>
                    <th className="n" title={`A mesma conta, somando o ano até o mês. Pede confirmação fora de ±${RD_FAIXA_ACUMULADO_PCT}%, a partir do ${RD_MESES_MINIMOS_ACUMULADO}º mês com venda.`}>Acumulado do ano</th>
                    <th>Situação</th>
                    <th></th>
                  </tr>
                </thead>
                <tbody>
                  {s.anos.map(g => (
                    <React.Fragment key={g.ano}>
                      {g.linhas.map(l => (
                        <React.Fragment key={l.r.id}>
                          <tr className="clickable" onClick={() => setAberta(x => (x === l.r.id ? null : l.r.id))}>
                            <td>{rdMesAno(l.r.periodo_inicio)}</td>
                            <td className="n">{rdFmt(l.bruto)} <span className="muted">({l.r.n_lancamentos})</span></td>
                            <td className="n">{rdFmt(l.v.liberado)}</td>
                            <td className="n" style={{ fontWeight: 600 }}>{rdFmt(l.taxa)}</td>
                            <td className="n">{rdPct(l.custoCanal)}</td>
                            <td className="n">{rdPct(l.descontoMerc)}</td>
                            <td className="n">{l.zero ? 'mês sem venda' : l.desvio == null ? 'sem venda' : rdPctSinal(l.desvio)}</td>
                            <td className="n" style={l.acumulado && l.acumulado.fora ? { color: 'var(--tm-warning)' } : undefined}>
                              {!l.acumulado ? '—' : l.acumulado.desvio == null ? 'sem venda' : rdPctSinal(l.acumulado.desvio)}
                              {l.faltaMes ? <span className="muted"> · falta mês</span> : null}
                            </td>
                            <td>
                              <div className="rd-selos">
                                {!l.baseMudou && !l.editada && !l.zero && <Badge tone="success">conferida</Badge>}
                                {l.zero && !l.baseMudou && <Badge tone="neutral">mês sem venda</Badge>}
                                {l.baseMudou && (
                                  <span title={`Na importação: ${rdFmt(l.bruto)} em ${l.r.n_lancamentos} lançamentos. Hoje: ${rdFmt(l.hoje.c)} em ${l.hoje.n}. A taxa gravada não acompanha: substitua reimportando o PDF.`}>
                                    <Badge tone="warning">base mudou</Badge>
                                  </span>
                                )}
                                {l.editada && <span title={l.editada}><Badge tone="danger">taxa editada em Contas a Pagar</Badge></span>}
                                {l.r.acumulado_confirmado && <Badge tone="neutral">acumulado confirmado</Badge>}
                                {l.r.desvio_confirmado && (
                                  <span title="Confirmado na regra antiga: desvio do mês fora de ±10%, na base preço do produto + frete.">
                                    <Badge tone="neutral">desvio do mês confirmado</Badge>
                                  </span>
                                )}
                                {l.reembolso.aviso && (
                                  <span title={rdTextoReembolso(l.reembolso)}><Badge tone="warning">reembolso {rdPct(l.reembolso.pct)}</Badge></span>
                                )}
                                {l.r.negativa_confirmada && <Badge tone="neutral">negativa confirmada</Badge>}
                              </div>
                            </td>
                            <td onClick={e => e.stopPropagation()} style={{ whiteSpace: 'nowrap' }}>
                              <Button variant="ghost" size="sm" icon="undo-2" disabled={!podeExcluir || desfazendo === l.r.id}
                                title={podeExcluir ? 'Apaga a declaração e a linha de taxa dela' : tmMotivoNegado('marketplace', 'excluir')}
                                onClick={() => desfazer(l)}>
                                {desfazendo === l.r.id ? 'Desfazendo…' : 'Desfazer'}
                              </Button>
                            </td>
                          </tr>
                          {aberta === l.r.id && (
                            <tr className="rd-detalhe">
                              <td colSpan={10}>
                                <div className="rd-detalhe-grid">
                                  {l.partes.map(p => (
                                    <div key={p.chave} title={p.dica}><div className="r">{p.nome}</div><div className="v">{rdFmt(p.c)}</div></div>
                                  ))}
                                  <div title="Não é uma das partes da taxa: ninguém sabe quanto da venda reembolsada está no bruto do app.">
                                    <div className="r">Ao lado, não dentro: reembolso e cupom</div>
                                    <div className="v">{rdTextoReembolsoCurto(l.reembolso)}</div>
                                  </div>
                                  <div>
                                    <div className="r">Arquivo</div>
                                    <div className="v">{l.r.arquivo_nome} · importado em {rdDataHora(l.r.importado_em)}
                                      {l.r.ajustes_total != null ? ` · ajuste fora da taxa ${rdFmt(rdCent(l.r.ajustes_total))} (pode ser de pedido de outro período, diz a Shopee)` : ''}</div>
                                  </div>
                                </div>
                                {l.reembolso.aviso && (
                                  <div className="bal-aviso warning"><Icon name="undo-2" size={15} /><div>{rdTextoReembolso(l.reembolso)}</div></div>
                                )}
                              </td>
                            </tr>
                          )}
                        </React.Fragment>
                      ))}
                      <tr className="rd-ano">
                        <td>Total {g.ano} · {g.linhas.length} {g.linhas.length === 1 ? 'mês' : 'meses'}</td>
                        <td className="n">{rdFmt(g.bruto)}</td>
                        <td className="n">{rdFmt(g.liberado)}</td>
                        <td className="n">{rdFmt(g.taxa)}</td>
                        <td className="n">{rdPct(g.bruto ? g.taxa / g.bruto * 100 : null)}</td>
                        <td className="n">{rdPct(g.mercadoria ? (g.mercadoria - g.liberado) / g.mercadoria * 100 : null)}</td>
                        <td></td>
                        <td className="n">{g.bruto ? rdPctSinal((g.mercFrete - g.bruto) / g.bruto * 100) : '—'}</td>
                        <td colSpan={2}></td>
                      </tr>
                    </React.Fragment>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        ))}
        {serie.length > 0 && (
          <div className="rd-nota">
            Clique num mês para ver os quatro pedaços da taxa, com o reembolso ao lado. "Custo total do canal" é o que vai
            para o DRE; "Desconto da Shopee sobre a mercadoria" usa só números da declaração — são bases diferentes, por
            isso os nomes diferentes. {RD_FAIXA_ORIGEM} {RD_FAIXA_LIMITE}
          </div>
        )}
      </Card>

      <Card title="O que falta" padded style={{ marginTop: 16 }}>
        {!base ? <div className="rd-nota">Carregando…</div> : (
          <>
            {faltaVenda.length > 0 && (
              <div className="bal-aviso warning" style={{ marginTop: 0 }}>
                <Icon name="alert-triangle" size={15} />
                <div>
                  <b>Falta lançar venda antes de importar</b> — pelas declarações soltas acima:
                  <ul className="rd-lista">
                    {faltaVenda.map(x => (
                      <li key={x.chave}>
                        {x.loja.nome} · {rdMesAno(x.an.d.inicio)}: {rdMotivoFaltaVenda(x.an).replace(/^./, c => c.toLowerCase())}.
                      </li>
                    ))}
                  </ul>
                  Importar antes de lançar grava uma taxa errada, e depois de importar o app não aceita mais lançar venda no período.
                </div>
              </div>
            )}
            {falta.shopee.length === 0 && <div className="rd-nota">Nenhuma loja Shopee com lançamento.</div>}
            {falta.shopee.map(f => (
              <div key={f.loja.id} className="rd-falta-loja">
                <div className="rd-falta-nome">{f.loja.nome}</div>
                {f.lista.length === 0 ? (
                  <div className="rd-nota" style={{ marginTop: 0 }}>Nada: {f.meses === 1 ? 'o mês com lançamento tem' : `os ${f.meses} meses com lançamento têm`} declaração ou taxa pelo repasse.</div>
                ) : (
                  <div className="rd-falta-meses">
                    {f.lista.map(m => (
                      <span key={m.k} className={'rd-mes' + (m.amarradas || faltaVendaMes.has(f.loja.id + '|' + m.k) ? ' misto' : '')}
                        title={m.amarradas
                          ? `${m.amarradas} de ${m.n} lançamentos deste mês já têm taxa pelo repasse: a declaração do mês seria recusada.`
                          : `${m.n} lançamento${m.n === 1 ? '' : 's'} sem taxa, ${rdFmt(m.c)} de bruto`}>
                        {rdMesAno(m.k + '-01')} · {rdFmt(m.c)}{m.amarradas ? ' · misto' : ''}{faltaVendaMes.has(f.loja.id + '|' + m.k) ? ' · falta lançar venda' : ''}
                      </span>
                    ))}
                  </div>
                )}
              </div>
            ))}
            {falta.outras.length > 0 && (
              <div className="rd-nota">
                Sem leitor de declaração: {falta.outras.map(o => `${o.loja.nome} (${o.loja.plataforma}, ${o.n} lançamento${o.n === 1 ? '' : 's'})`).join(' · ')}.
                A taxa dessas lojas segue só pelo registro de repasse, em Marketplace › Lançamentos.
              </div>
            )}
            <div className="rd-nota">
              Mês sem nenhum lançamento não aparece nesta lista. Para saber se falta lançar venda num mês, solte a
              declaração dele acima: a conferência compara o app com a Shopee antes de gravar.
            </div>
          </>
        )}
      </Card>

      {modalAberto && (
        <ModalCentral eyebrow="Marketplace · Rendimentos" title={`Conferir e gravar ${prontos.length}`} width={1080}
          subtitle="A conferência é a única defesa contra venda que faltou no app: a subtração absorve o erro."
          onClose={() => { if (!gravando) setModalAberto(false); }}
          footer={<>
            <label className="rd-confirma fc-foot-info" style={{ margin: 0 }}>
              <RdCaixa checked={conferiu} onMarcar={setConferiu} />
              <span>Conferi, linha a linha, o bruto do app contra a Shopee (mercadoria + frete do comprador) e o acumulado do ano.</span>
            </label>
            <Button variant="secondary" size="sm" disabled={gravando} onClick={() => setModalAberto(false)}>Voltar</Button>
            <Button variant="primary" size="sm" icon="check" disabled={!conferiu || gravando || prontos.length === 0} onClick={gravar}>
              {gravando ? 'Gravando…' : `Gravar ${prontos.length}`}
            </Button>
          </>}>
          <div className="rd-tbl-wrap">
            <table className="tbl rd-tbl">
              <thead>
                <tr>
                  <th>Loja</th><th>Mês</th>
                  <th className="n">Bruto do app</th><th className="n">Shopee: mercadoria + frete</th>
                  <th className="n">Desvio do mês</th><th className="n">Acumulado do ano</th>
                  <th className="n">Liberado</th><th className="n">Taxa a gravar</th><th>Confirmado</th>
                </tr>
              </thead>
              <tbody>
                {prontos.map(a => {
                  const an = analise[a.chave], d = a.leitura.dados;
                  return (
                    <tr key={a.chave}>
                      <td>{an.loja.nome}</td>
                      <td>{rdMesAno(d.inicio)}</td>
                      <td className="n">{rdFmt(an.bruto.c)} <span className="muted">({an.bruto.n})</span></td>
                      <td className="n">{rdFmt(rdBaseShopee(d.valores))}</td>
                      <td className="n">{an.zero ? 'mês sem venda' : an.desvio == null ? 'sem venda' : rdPctSinal(an.desvio)}</td>
                      <td className="n" style={an.fora ? { color: 'var(--tm-warning)' } : undefined}>{rdTextoAcumulado(an)}</td>
                      <td className="n">{rdFmt(d.valores.liberado)}</td>
                      <td className="n" style={{ fontWeight: 600 }}>{rdFmt(an.taxa)}</td>
                      <td>{[an.existente && 'substitui', an.fora && 'acumulado', an.negativa && 'negativa', an.zero && 'mês sem venda'].filter(Boolean).join(' · ') || '—'}</td>
                    </tr>
                  );
                })}
              </tbody>
            </table>
          </div>
          <div className="rd-nota">
            Cada declaração é uma transação no banco, que confere tudo de novo — inclusive se o bruto do app mudou desde
            esta tela. Grava em ordem, loja por loja e mês a mês: a recusada não grava nada, as outras lojas seguem, e os
            meses seguintes da mesma loja no mesmo ano não são enviados. {RD_FAIXA_LIMITE}
          </div>
        </ModalCentral>
      )}
    </div>
  );
}

Object.assign(window, {
  MarketplaceRendimentos, rdLerDeclaracaoShopee, rdCentavos, rdNorm, rdDecomposicao, rdDesvio, rdReembolso,
  rdAcumulado, rdAnalisar, RD_FAIXA_ACUMULADO_PCT, RD_MESES_MINIMOS_ACUMULADO, RD_REEMBOLSO_AVISO_PCT, RD_PDFJS_VERSAO,
});
