<?php
/**
 * Depto de Engenharia e Automação - Master Automação - Autor: Derlivon Silva
 * (parte do Premium Express BI - Fire Sistemas)
 */

namespace App\Repositories;

use App\Core\Cache;

class VendasRepository
{
    /** Coluna de desconto do ERP — descoberta uma vez e reutilizada na sessão (OPcache) */
    private static ?string $colunaDesconto = null;

    /** Cache dos tipos de operação de venda válidos POR EMPRESA (1 por request) */
    private static array $tiposVendaInPorEmp = [];

    /** Coluna de hora da nota — descoberta uma vez e persistida no cache de arquivo */
    private static ?string $colunaHora = null;

    /** COD_EMPRESA configurado (null = sem filtro) */
    private static ?int $codEmpresa = null;
    private static bool $codEmpresaCarregado = false;

    private function filtroEmpresa(string $alias = 'nc'): string
    {
        if (!self::$codEmpresaCarregado) {
            self::$codEmpresaCarregado = true;
            try {
                self::$codEmpresa = \App\Core\Lojas::codEmpresaAtiva() ?: null;
            } catch (\Throwable) {
                self::$codEmpresa = null;
            }
        }
        if (!self::$codEmpresa) return '';
        $a = $alias !== '' ? "{$alias}." : '';
        return "AND {$a}COD_EMPRESA = " . self::$codEmpresa;
    }

    public function __construct(private \PDO $pdo) {}

    /**
     * Condição de intervalo de datas segura para colunas DATE e TIMESTAMP.
     * BETWEEN '2026-05-22' limita a meia-noite quando a coluna é TIMESTAMP;
     * este método usa >= ini AND < fim+1 para incluir o dia inteiro.
     */
    private function intervaloData(string $ini, string $fim, string $alias = 'nc'): string
    {
        $col = $alias !== '' ? "{$alias}.DATA_EMISSAO" : 'DATA_EMISSAO';
        return "{$col} >= '{$ini}' AND {$col} < DATEADD(DAY, 1, CAST('{$fim}' AS DATE))";
    }

    /**
     * Retorna fragmento SQL "AND <alias>.COD_TIPO_OPERACAO IN (...)"
     * filtrando apenas vendas reais — exclui compras, transferências,
     * devoluções e movimentos internos.
     * Fallback silencioso: sem filtro se TIPO_OPERACAO não existir.
     */
    private function filtroVenda(string $alias = 'nc'): string
    {
        // Cadastro de TIPO_OPERACAO é POR EMPRESA (PK = COD_TIPO_OPERACAO+COD_EMPRESA).
        // Cada empresa pode ter flags diferentes p/ a mesma operação → o filtro tem que
        // bater na empresa ATIVA. Sem empresa ativa, cai no global (DISTINCT).
        // Chamamos filtroEmpresa() apenas pra garantir que self::$codEmpresa esteja carregado.
        $this->filtroEmpresa($alias);
        $emp = self::$codEmpresa;
        $cacheKey = $emp ? (string)$emp : '_global';

        if (!array_key_exists($cacheKey, self::$tiposVendaInPorEmp)) {
            // Vendas REAIS = natureza 'V', fora devolução/transferência/mov. interna E
            // que GERAM FINANCEIRO (GERA_FINANCEIRO='S'). Isto exclui "venda para grupo/
            // coligada", uso e consumo, baixas etc. — que o ERP não conta como faturamento.
            // Fallback: se a coluna GERA_FINANCEIRO não existir, usa o filtro antigo.
            $empWhere    = $emp ? " AND COD_EMPRESA = {$emp}"  : '';
            $empWhereSub = $emp ? " AND COD_EMPRESA = {$emp}"  : '';
            $sqlBase = "
                SELECT DISTINCT COD_TIPO_OPERACAO FROM TIPO_OPERACAO
                WHERE TIPO_NATUREZA = 'V'{$empWhere}
                  AND COD_TIPO_OPERACAO NOT IN (
                      SELECT COD_TIPO_OPERACAO FROM TIPO_OPERACAO
                      WHERE (DEVOLUCAO = 'S' OR ETRANSF = 'S' OR MOV_INTERNA = 'S' %s){$empWhereSub}
                  )";
            $found = '';
            try {
                $rows = $this->pdo->query(sprintf($sqlBase, "OR GERA_FINANCEIRO = 'N'"))->fetchAll(\PDO::FETCH_COLUMN);
                $found = !empty($rows) ? implode(',', array_map('intval', $rows)) : '';
            } catch (\Throwable) {
                try {
                    $rows = $this->pdo->query(sprintf($sqlBase, ''))->fetchAll(\PDO::FETCH_COLUMN);
                    $found = !empty($rows) ? implode(',', array_map('intval', $rows)) : '';
                } catch (\Throwable) { $found = ''; }
            }
            self::$tiposVendaInPorEmp[$cacheKey] = $found;
        }
        $in = self::$tiposVendaInPorEmp[$cacheKey];

        // === OVERRIDES por cliente (config/database.php) ===
        // Permite adaptar o BI a ERPs configurados de forma "atípica" sem mexer
        // no cadastro do ERP do cliente.
        //   'tipos_venda_extras'  => [115, 168, ...]  // INCLUI estas ops como venda
        //   'tipos_venda_excluir' => [291, ...]        // EXCLUI estas mesmo que passariam
        $cfg     = $this->configCliente();
        $extras  = (array)($cfg['tipos_venda_extras']  ?? []);
        $excluir = (array)($cfg['tipos_venda_excluir'] ?? []);
        if ($extras || $excluir) {
            $ops = $in !== '' ? array_map('intval', explode(',', $in)) : [];
            foreach ($extras as $e)  if (!in_array((int)$e, $ops, true)) $ops[] = (int)$e;
            $ops = array_values(array_diff($ops, array_map('intval', $excluir)));
            $in  = $ops ? implode(',', $ops) : '';
        }

        if (!$in) return '';
        $a = $alias !== '' ? "{$alias}." : '';
        return "AND {$a}COD_TIPO_OPERACAO IN (" . $in . ")";
    }

    /** Cache estático do config do cliente (config/database.php) pra overrides. */
    private function configCliente(): array
    {
        static $cache = null;
        if ($cache !== null) return $cache;
        $path = dirname(__DIR__, 2) . '/config/database.php';
        $cache = file_exists($path) ? (array)(@include $path) : [];
        return $cache;
    }

    // -------------------------------------------------------------------------
    // API pública dos filtros de venda — para reuso fora deste repositório
    // (PrevisaoController, ClientesRepository, etc.). Mantém filtroVenda() e
    // filtroEmpresa() PRIVATE pra não quebrar callers internos; nome diferente
    // sinaliza claramente "esta é a entrada pública". Cache static $codEmpresa
    // / $tiposVendaInPorEmp continua compartilhado entre instâncias do mesmo
    // request — chave por COD_EMPRESA evita queries repetidas.
    // -------------------------------------------------------------------------

    /**
     * Fragmento "AND <alias>.COD_TIPO_OPERACAO IN (...)" com extras/excluir
     * de config/database.php já aplicados. Use o mesmo alias da tabela NOTAS_CAB
     * na query chamadora (padrão 'nc'); passe '' quando a query não usa alias.
     */
    public function filtroVendaSql(string $alias = 'nc'): string
    {
        return $this->filtroVenda($alias);
    }

    /**
     * Fragmento "AND <alias>.COD_EMPRESA = X" baseado em Lojas::codEmpresaAtiva().
     * Fonte única de verdade para o filtro de empresa — substitui leituras
     * diretas de $cfg['cod_empresa'] feitas em outros controllers.
     */
    public function filtroEmpresaSql(string $alias = 'nc'): string
    {
        return $this->filtroEmpresa($alias);
    }

    /** TTL: 60s para dados de hoje (alinhado com auto-refresh JS de 60s — v1.4.7),
     *  30 min para períodos passados.
     *  Histórico: v1.4.0=600s, v1.4.1=30s (incômodo na reconciliação), v1.4.7=60s. */
    private function ttl(string $dataFim): int
    {
        return ($dataFim >= date('Y-m-d')) ? 60 : 1800;
    }

    // -------------------------------------------------------------------------
    // KPIs de Gestão de Vendas
    // -------------------------------------------------------------------------

    public function kpisGestao(string $dataIni, string $dataFim, ?int $codVendedor = null): array
    {
        $filtroVend = $codVendedor ? "AND nc.COD_PESSOA_VENDA = {$codVendedor}" : '';
        $cacheKey   = "vendas_kpis_{$dataIni}_{$dataFim}_" . ($codVendedor ?? '0');

        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $filtroVend) {
            // 1 query: faturamento + qtd combinados
            $fv  = $this->filtroVenda('nc');
            $fe  = $this->filtroEmpresa('nc');
            $row = $this->pdo->query("
                SELECT
                    COALESCE(SUM(CASE WHEN nc.TOTAL_NOTA > 0 THEN nc.TOTAL_NOTA ELSE 0 END), 0) AS FATURAMENTO,
                    COUNT(CASE WHEN nc.TOTAL_NOTA > 0 THEN 1 END)                                AS QTD_VENDAS
                FROM NOTAS_CAB nc
                WHERE {$this->intervaloData($dataIni, $dataFim, 'nc')}
                  AND nc.DATA_CANC IS NULL
                  {$fv}
                  {$fe}
                  {$filtroVend}
            ")->fetch(\PDO::FETCH_ASSOC);

            $faturamento = (float)($row['FATURAMENTO'] ?? 0);
            $qtdVendas   = (int)($row['QTD_VENDAS']   ?? 0);
            $ticketMedio = $qtdVendas > 0 ? round($faturamento / $qtdVendas, 2) : 0.0;

            // Desconto: reutiliza coluna já descoberta (evita tentativas repetidas)
            $desconto = 0.0;
            $cols = self::$colunaDesconto
                ? [self::$colunaDesconto]
                : ['DESCONTO', 'VLR_DESCONTO', 'TOTAL_DESCONTO', 'DESCONTO_TOTAL'];
            foreach ($cols as $col) {
                try {
                    $v = (float) $this->escalar("
                        SELECT COALESCE(SUM(nc.{$col}), 0)
                        FROM NOTAS_CAB nc
                        WHERE {$this->intervaloData($dataIni, $dataFim, 'nc')}
                          AND nc.DATA_CANC IS NULL
                          {$fv}
                          {$fe}
                          {$filtroVend}
                    ");
                    self::$colunaDesconto = $col;
                    $desconto = $v;
                    break;
                } catch (\Throwable) {}
            }

            // Cancelamentos (1 query com COUNT + SUM)
            $qtdCanceladas = 0;
            $vlrCanceladas = 0.0;
            try {
                $rowCanc = $this->pdo->query("
                    SELECT COUNT(*) AS QTD, COALESCE(SUM(TOTAL_NOTA), 0) AS VLR
                    FROM NOTAS_CAB nc
                    WHERE {$this->intervaloData($dataIni, $dataFim, 'nc')}
                      AND nc.DATA_CANC IS NOT NULL
                      {$fv}
                      {$fe}
                      {$filtroVend}
                ")->fetch(\PDO::FETCH_ASSOC);
                $qtdCanceladas = (int)($rowCanc['QTD'] ?? 0);
                $vlrCanceladas = (float)($rowCanc['VLR'] ?? 0);
            } catch (\Throwable) {}

            $dias     = max(1, (int)((strtotime($dataFim) - strtotime($dataIni)) / 86400) + 1);
            $mediaDia = round($faturamento / $dias, 2);

            return [
                'faturamento'    => $faturamento,
                'qtd_vendas'     => $qtdVendas,
                'ticket_medio'   => $ticketMedio,
                'desconto'       => $desconto,
                'qtd_canceladas' => $qtdCanceladas,
                'vlr_canceladas' => $vlrCanceladas,
                'media_dia'      => $mediaDia,
                'dias_periodo'   => $dias,
            ];
        });
    }

    // -------------------------------------------------------------------------
    // Faturamento + ticket médio por hora (gráfico)
    // -------------------------------------------------------------------------

    public function faturamentoPorHora(string $dataIni, string $dataFim, ?int $codVendedor = null): array
    {
        $filtroVend = $codVendedor ? "AND COD_PESSOA_VENDA = {$codVendedor}" : '';
        $cacheKey   = "vendas_hora_{$dataIni}_{$dataFim}_" . ($codVendedor ?? '0');

        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $filtroVend) {
            // Descobre coluna de hora uma única vez e persiste em cache de arquivo
            if (self::$colunaHora === null) {
                self::$colunaHora = Cache::lembrar('vendas_coluna_hora', 86400, function () {
                    foreach (['DATA_HORA_VENDA', 'HORA_VENDA', 'DATA_HORA', 'DT_HORA_EMISSAO'] as $col) {
                        try {
                            $this->pdo->query("SELECT FIRST 1 EXTRACT(HOUR FROM {$col}) FROM NOTAS_CAB WHERE {$col} IS NOT NULL")->fetch();
                            return $col;
                        } catch (\Throwable) {}
                    }
                    return '';
                }) ?? '';
            }

            if (self::$colunaHora === '') return [];

            $col = self::$colunaHora;
            try {
                return $this->pdo->query("
                    SELECT
                        EXTRACT(HOUR FROM {$col})        AS HORA,
                        COALESCE(SUM(TOTAL_NOTA), 0)     AS FATURAMENTO,
                        COUNT(*)                          AS QTD,
                        COALESCE(AVG(TOTAL_NOTA), 0)      AS TICKET_MEDIO
                    FROM NOTAS_CAB
                    WHERE {$this->intervaloData($dataIni, $dataFim, '')}
                      AND DATA_CANC IS NULL
                      AND {$col} IS NOT NULL
                      AND TOTAL_NOTA > 0
                      {$this->filtroVenda('')}
                      {$this->filtroEmpresa('')}
                      {$filtroVend}
                    GROUP BY EXTRACT(HOUR FROM {$col})
                    ORDER BY HORA
                ")->fetchAll();
            } catch (\Throwable) { return []; }
        });
    }

    // -------------------------------------------------------------------------
    // Formas de pagamento detalhadas
    // -------------------------------------------------------------------------

    public function formasPagamento(string $dataIni, string $dataFim, ?int $codVendedor = null): array
    {
        $filtroVend = $codVendedor ? "AND nc.COD_PESSOA_VENDA = {$codVendedor}" : '';
        $cacheKey   = "vendas_formas_{$dataIni}_{$dataFim}_" . ($codVendedor ?? '0');

        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $filtroVend) {
            try {
                // UPPER unifica variantes de capitalização (ex.: "Pix" e "PIX" cadastrados
                // duplicado no ERP viram uma linha só no agrupamento).
                return $this->pdo->query("
                    SELECT
                        UPPER(TRIM(fp.DESCRICAO))       AS FORMA,
                        COALESCE(fp.TIPO, '4')          AS TIPO,
                        COALESCE(fp.CORTESIA, 'N')      AS CORTESIA,
                        SUM(fn.VALOR)                   AS TOTAL
                    FROM FORMAS_NOTAS fn
                    JOIN FORMA_PGTO fp  ON fp.COD_FORMA_PGTO  = fn.COD_FORMA_PGTO
                    JOIN NOTAS_CAB nc   ON nc.NR_NOTA          = fn.NR_NOTA
                                       AND nc.SERIE             = fn.SERIE
                                       AND nc.COD_EMPRESA       = fn.COD_EMPRESA
                                       AND nc.COD_TIPO_OPERACAO = fn.COD_TIPO_OPERACAO
                    WHERE {$this->intervaloData($dataIni, $dataFim, 'nc')}
                      AND nc.DATA_CANC IS NULL
                      {$this->filtroVenda('nc')}
                      {$this->filtroEmpresa('nc')}
                      {$filtroVend}
                    GROUP BY UPPER(TRIM(fp.DESCRICAO)), fp.TIPO, fp.CORTESIA
                    ORDER BY TOTAL DESC
                ")->fetchAll();
            } catch (\Throwable) { return []; }
        });
    }

    /** Agrupamento Dinheiro / Débito / Crédito */
    private function u8pg(string $s): string
    {
        return ($s !== '' && !mb_check_encoding($s, 'UTF-8')) ? mb_convert_encoding($s, 'UTF-8', 'Windows-1252') : $s;
    }

    /** Normaliza a forma de pagamento — agrupa por NOME pra evitar duplicidades (PIX vs TEF PIX) e
     *  separar CORTESIA do Dinheiro (mesmo que tenham mesmo TIPO no cadastro do Fire).
     *  Coerente com a categorização do Mix de Pagamento (FinanceiroRepository::mixPagamentoCompleto).
     */
    private function labelPgto(string $tipo, string $desc): string
    {
        $d = mb_strtoupper($this->u8pg($desc), 'UTF-8');
        // 1) Casos por NOME (prioritário) — alinhado com Mix de Pagamento
        if (str_contains($d, 'CORTESIA'))   return 'Cortesia';
        if (str_contains($d, 'PEDIAI') || str_contains($d, 'IFOOD') || str_contains($d, 'AIQ')) return 'Delivery';
        if (str_contains($d, 'PIX'))        return 'PIX';
        if (str_contains($d, 'DEBITO') || str_contains($d, 'DÉBITO')) return 'Débito';
        if (str_contains($d, 'CREDIARIO') || str_contains($d, 'FATURAMENTO') || str_contains($d, 'BOLETO')) return 'A Prazo';
        if (str_contains($d, 'CRED'))       return 'Crédito';
        if (str_contains($d, 'CHEQUE'))     return 'Cheque';
        if (str_contains($d, 'DINHEIRO'))   return 'Dinheiro';
        // 2) Fallbacks por TIPO (se o nome não bateu nada)
        if ($tipo === '1') return 'Dinheiro';
        if ($tipo === '8') return 'PIX';
        if ($tipo === '2') return 'Cheque';
        // 3) Último recurso: nome do cadastro em Title Case
        return mb_convert_case(mb_strtolower($this->u8pg($desc), 'UTF-8'), MB_CASE_TITLE, 'UTF-8');
    }

    public function tiposPagamento(string $dataIni, string $dataFim, ?int $codVendedor = null): array
    {
        $filtroVend = $codVendedor ? "AND nc.COD_PESSOA_VENDA = {$codVendedor}" : '';
        $cacheKey   = "vendas_tipos_pgto_{$dataIni}_{$dataFim}_" . ($codVendedor ?? '0');

        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $filtroVend) {
            try {
                $rows = $this->pdo->query("
                    SELECT
                        fp.TIPO,
                        UPPER(TRIM(fp.DESCRICAO))  AS DESCRICAO,
                        SUM(fn.VALOR)              AS TOTAL
                    FROM FORMAS_NOTAS fn
                    JOIN FORMA_PGTO fp  ON fp.COD_FORMA_PGTO  = fn.COD_FORMA_PGTO
                    JOIN NOTAS_CAB nc   ON nc.NR_NOTA          = fn.NR_NOTA
                                       AND nc.SERIE             = fn.SERIE
                                       AND nc.COD_EMPRESA       = fn.COD_EMPRESA
                                       AND nc.COD_TIPO_OPERACAO = fn.COD_TIPO_OPERACAO
                    WHERE {$this->intervaloData($dataIni, $dataFim, 'nc')}
                      AND nc.DATA_CANC IS NULL
                      {$this->filtroVenda('nc')}
                      {$this->filtroEmpresa('nc')}
                      {$filtroVend}
                    GROUP BY fp.TIPO, UPPER(TRIM(fp.DESCRICAO))
                ")->fetchAll();

                // Formas REAIS (PIX, Débito, Crédito, POS Débito/Crédito, Dinheiro...).
                $itens = [];
                foreach ($rows as $r) {
                    $val = (float)$r['TOTAL'];
                    if ($val == 0) continue;
                    $lbl = $this->labelPgto((string)$r['TIPO'], (string)$r['DESCRICAO']);
                    $itens[$lbl] = ($itens[$lbl] ?? 0) + $val;
                }
                arsort($itens);
                // SEM "Outros": lista TODAS as formas reais de pagamento (cada uma com seu valor).
                $out = [];
                foreach ($itens as $lbl => $tot) {
                    $out[] = ['label' => $lbl, 'total' => round($tot, 2)];
                }
                return $out;
            } catch (\Throwable) { return []; }
        });
    }

    /** Dinheiro / Débito / Crédito agrupados por MÊS do ano (gráfico empilhado mensal). */
    public function tiposPagamentoMensal(int $ano): array
    {
        $cacheKey = "vendas_tipos_pgto_mensal_{$ano}";
        $ttl      = ($ano === (int)date('Y')) ? 300 : 1800;

        return Cache::lembrar($cacheKey, $ttl, function () use ($ano) {
            $ini = "{$ano}-01-01";
            $fim = "{$ano}-12-31";
            try {
                $rows = $this->pdo->query("
                    SELECT
                        EXTRACT(MONTH FROM nc.DATA_EMISSAO) AS MES,
                        fp.TIPO,
                        UPPER(TRIM(fp.DESCRICAO))  AS DESCRICAO,
                        SUM(fn.VALOR)              AS TOTAL
                    FROM FORMAS_NOTAS fn
                    JOIN FORMA_PGTO fp  ON fp.COD_FORMA_PGTO  = fn.COD_FORMA_PGTO
                    JOIN NOTAS_CAB nc   ON nc.NR_NOTA          = fn.NR_NOTA
                                       AND nc.SERIE             = fn.SERIE
                                       AND nc.COD_EMPRESA       = fn.COD_EMPRESA
                                       AND nc.COD_TIPO_OPERACAO = fn.COD_TIPO_OPERACAO
                    WHERE {$this->intervaloData($ini, $fim, 'nc')}
                      AND nc.DATA_CANC IS NULL
                      {$this->filtroVenda('nc')}
                      {$this->filtroEmpresa('nc')}
                    GROUP BY EXTRACT(MONTH FROM nc.DATA_EMISSAO), fp.TIPO, UPPER(TRIM(fp.DESCRICAO))
                ")->fetchAll();
            } catch (\Throwable) { return []; }

            $meses = [];
            foreach ($rows as $r) {
                $m = (int)$r['MES'];
                if (!isset($meses[$m])) $meses[$m] = ['MES' => $m, 'dinheiro' => 0.0, 'debito' => 0.0, 'credito' => 0.0];
                $tipo = (string)$r['TIPO'];
                $desc = strtoupper((string)$r['DESCRICAO']);
                $val  = (float)$r['TOTAL'];
                if ($tipo === '1' || $tipo === '8') {            // dinheiro / PIX
                    $meses[$m]['dinheiro'] += $val;
                } elseif ($tipo === '3') {
                    if (str_contains($desc, 'DEBITO') || str_contains($desc, 'DÉBITO')) $meses[$m]['debito'] += $val;
                    else $meses[$m]['credito'] += $val;
                }
            }
            ksort($meses);
            return array_values($meses);
        });
    }

    /**
     * Decide se o CMV agregado pode ser CONFIADO pra mostrar Lucro/Margem.
     * Aplicado quando o ERP do cliente não grava CUSTO_FINAL direito (bar/distribuidora
     * sem custo no cadastro, ou ERP que grava custo de caixa fechada em cada unidade).
     *
     * $cmvArray  — saída de cmvMensal(): linhas com chaves MES, CMV, FAT
     * $vendasArr — saída de vendasMensais(): linhas com chaves MES, FATURAMENTO/VL_TOTAL
     *
     * Retorna FALSE quando:
     *   - SUM(CMV) >= SUM(FAT) × 1.0     (custo total ≥ faturamento — não tem lucro)
     *   - OU mais de 50% dos meses com FAT>0 tiveram CMV ≥ FAT (sinal sistêmico)
     *
     * Conservador: na dúvida, esconde Lucro/Margem em vez de mostrar número errado.
     */
    public function cmvConfiavel(array $cmvArray, array $vendasArr = []): bool
    {
        $totCmv = 0.0; $totFat = 0.0; $mesesRuins = 0; $mesesValidos = 0;
        // Index do faturamento "oficial" por mês (vendasMensais é a fonte da verdade do faturamento)
        $fatOficial = [];
        foreach ($vendasArr as $r) {
            $m = (int)($r['MES'] ?? 0);
            $v = (float)($r['FATURAMENTO'] ?? $r['VL_TOTAL'] ?? $r['FAT'] ?? 0);
            if ($m > 0) $fatOficial[$m] = $v;
        }
        foreach ($cmvArray as $r) {
            $m   = (int)($r['MES'] ?? 0);
            $cmv = (float)($r['CMV'] ?? 0);
            $fat = $fatOficial[$m] ?? (float)($r['FAT'] ?? 0);
            $totCmv += $cmv; $totFat += $fat;
            if ($fat > 0) {
                $mesesValidos++;
                if ($cmv >= $fat) $mesesRuins++;
            }
        }
        if ($totFat <= 0 && $totCmv <= 0) return true; // sem dados ainda → não atrapalha
        if ($totCmv >= $totFat) return false;
        if ($mesesValidos > 0 && ($mesesRuins / $mesesValidos) > 0.5) return false;
        return true;
    }

    /**
     * CMV e faturamento (a partir dos ITENS vendidos) por MÊS — base p/ lucro e margem REAIS.
     * Usa ni.CUSTO_FINAL (custo gravado na venda). Pesado (varre os itens do ano) → cacheado.
     * Sem JOIN com NOTAS_CAB (NOTAS_ITENS já tem empresa/data/operação próprios → usa índice).
     */
    public function cmvMensal(int $ano): array
    {
        $cacheKey = "vendas_cmv_mensal_{$ano}";
        $ttl      = ($ano === (int)date('Y')) ? 1800 : 86400;

        return Cache::lembrar($cacheKey, $ttl, function () use ($ano) {
            $emp = 0;
            try { $emp = (int)\App\Core\Lojas::codEmpresaAtiva(); } catch (\Throwable) {}
            $ini = "{$ano}-01-01";
            $fim = "{$ano}-12-31";
            $filtroEmp = $emp > 0 ? "ni.COD_EMPRESA = {$emp} AND" : '';
            try {
                return $this->pdo->query("
                    SELECT EXTRACT(MONTH FROM ni.DATA_EMISSAO) AS MES,
                           SUM(ni.QTD * ni.CUSTO_FINAL)  AS CMV,
                           SUM(ni.QTD * ni.PRECO_UNIT)   AS FAT
                    FROM NOTAS_ITENS ni
                    WHERE {$filtroEmp} ni.DATA_EMISSAO BETWEEN '{$ini}' AND '{$fim}'
                      AND ni.COD_TIPO_OPERACAO IN (115,231,297)
                    GROUP BY EXTRACT(MONTH FROM ni.DATA_EMISSAO)
                    ORDER BY MES
                ")->fetchAll(\PDO::FETCH_ASSOC);
            } catch (\Throwable) { return []; }
        });
    }

    // =========================================================================
    // RELATÓRIO DE VENDAS (configurável) — espelha o relatório do ERP
    // =========================================================================

    /** Listas para os selects de filtro (vendedores, naturezas, condições, formas, agentes, usuários, séries). */
    public function relatorioVendasLookups(): array
    {
        $this->filtroEmpresa('nc'); // garante self::$codEmpresa carregado
        return Cache::lembrar('rel_vendas_lookups' . self::escopo(), 1800, function () {
            $emp = self::$codEmpresa;
            $fe  = $emp ? "WHERE COD_EMPRESA = {$emp}" : '';
            $out = ['vendedores'=>[], 'naturezas'=>[], 'condicoes'=>[], 'formas'=>[], 'agentes'=>[], 'usuarios'=>[], 'series'=>[]];
            $try = function (string $sql) { try { return $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC); } catch (\Throwable) { return []; } };

            // Vendedores: direto da PESSOAS pelo flag EVEND (instantâneo) — NUNCA varrer NOTAS_CAB.
            // Séries vêm da tabela de operações (minúscula).
            $out['vendedores'] = $try("SELECT COD_PESSOA COD, TRIM(NOME) NOME FROM PESSOAS
                WHERE EVEND = 'S' " . ($emp ? "AND COD_EMPRESA = {$emp}" : '') . " ORDER BY 2");
            $out['naturezas'] = $try("SELECT DISTINCT t.COD_NATUREZA COD, TRIM(t.DESCRICAO) NOME FROM TIPO_OPERACAO t {$fe} ORDER BY 2");
            $out['condicoes'] = $try("SELECT COD_COND_PGTO COD, TRIM(DESCRICAO) NOME FROM CONDICAO_PGTO ORDER BY 2");
            $out['formas']    = $try("SELECT COD_FORMA_PGTO COD, TRIM(DESCRICAO) NOME FROM FORMA_PGTO ORDER BY 2");
            $out['agentes']   = $try("SELECT COD_AGENTE COD, TRIM(NOME) NOME FROM AGENTES WHERE ATIVO = 'S' OR ATIVO IS NULL ORDER BY 2");
            $out['usuarios']  = $try("SELECT TRIM(COD_USUARIO) COD, TRIM(COALESCE(NOME,COD_USUARIO)) NOME FROM USUARIOS " . ($emp ? "WHERE COD_EMPRESA={$emp}" : '') . " ORDER BY 2");
            $out['series']    = $try("SELECT DISTINCT TRIM(SERIE) COD FROM TIPO_OPERACAO WHERE SERIE IS NOT NULL AND TRIM(SERIE) <> '' " . ($emp ? "AND COD_EMPRESA={$emp}" : '') . " ORDER BY 1");

            // Converte encoding dos nomes (Windows-1252 → UTF-8)
            foreach (['vendedores','naturezas','condicoes','formas','agentes','usuarios'] as $k) {
                foreach ($out[$k] as &$r) { if (isset($r['NOME'])) $r['NOME'] = mb_convert_encoding((string)$r['NOME'], 'UTF-8', 'Windows-1252'); }
                unset($r);
            }
            return $out;
        });
    }

    private static function escopo(): string
    {
        try { return '_' . (\App\Core\Lojas::codEmpresaAtiva() ?: '0'); } catch (\Throwable) { return '_0'; }
    }

    /**
     * Relatório de Vendas configurável. $f = filtros (todos opcionais).
     * Retorna resumo + quebras (vendedor/natureza/série/forma) + lista de notas (+produtos).
     */
    public function relatorioVendas(array $f): array
    {
        $this->filtroEmpresa('nc'); // garante self::$codEmpresa carregado
        $emp = self::$codEmpresa;

        // Cache curto por combinação de filtros (repetições/exports ficam instantâneos)
        $ck = 'relv_' . md5(json_encode($f)) . self::escopo();
        $cached = \App\Core\Cache::get($ck);
        if ($cached !== null) return $cached;

        // ── sanitização ──
        $D = function ($v) { return (is_string($v) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $v)) ? $v : null; };
        $I = function ($v) { return ($v === null || $v === '' || !is_numeric($v)) ? null : (int)$v; };
        $S = function ($v) { return "'" . str_replace("'", "''", trim((string)$v)) . "'"; };

        $ini = $D($f['data_ini'] ?? null) ?: date('Y-m-01');
        $fim = $D($f['data_fim'] ?? null) ?: date('Y-m-d');
        if ($fim < $ini) $fim = $ini;
        $campoData = ['emissao'=>'DATA_EMISSAO','cancel'=>'DATA_CANC','datahora'=>'DATA_HORA_VENDA'][$f['data_campo'] ?? 'emissao'] ?? 'DATA_EMISSAO';

        // IMPORTANTE: nada de CAST na coluna (anula o índice → full scan). Range simples = indexado.
        if ($campoData === 'DATA_EMISSAO') {
            $w = [$this->intervaloData($ini, $fim, 'nc')];
        } else {
            $w = ["nc.{$campoData} >= '{$ini}' AND nc.{$campoData} < DATEADD(DAY, 1, CAST('{$fim}' AS DATE))"];
        }
        if ($emp) $w[] = "nc.COD_EMPRESA = {$emp}";

        // Canceladas
        $canc = $f['canceladas'] ?? 'excluir';
        if ($canc === 'somente')      $w[] = "(nc.COD_SITUACAO = 3 OR nc.DATA_CANC IS NOT NULL)";
        elseif ($canc !== 'incluir')  $w[] = "nc.COD_SITUACAO <> 3 AND nc.DATA_CANC IS NULL";

        // Natureza / base de vendas
        $nats = array_filter(array_map('intval', (array)($f['naturezas'] ?? [])));
        if ($nats) {
            $w[] = "t.COD_NATUREZA IN (" . implode(',', $nats) . ")";
        } elseif ($I($f['nat_ini'] ?? null) !== null || $I($f['nat_fim'] ?? null) !== null) {
            $a = $I($f['nat_ini'] ?? null) ?? 0; $b = $I($f['nat_fim'] ?? null) ?? 999999;
            $w[] = "t.COD_NATUREZA BETWEEN {$a} AND {$b}";
        } else {
            $w[] = "t.TIPO_NATUREZA = 'V'"; // padrão: vendas (saídas)
        }

        // Movimentação interna / transferências / devoluções (padrão: não listar)
        if (empty($f['inc_mov_interna']))   $w[] = "(t.MOV_INTERNA = 'N' OR t.MOV_INTERNA IS NULL)";
        if (empty($f['inc_transferencias'])) $w[] = "(t.ETRANSF = 'N' OR t.ETRANSF IS NULL)";
        if (empty($f['inc_devolucoes']))     $w[] = "(t.DEVOLUCAO = 'N' OR t.DEVOLUCAO IS NULL)";
        if (!empty($f['so_gera_financeiro'])) $w[] = "t.GERA_FINANCEIRO = 'S'";
        if (!empty($f['ignora_naodre']))      $w[] = "(t.NAO_VAI_DRE = 'N' OR t.NAO_VAI_DRE IS NULL)";

        // Flags de documento
        if (!empty($f['nfce'])) $w[] = "nc.ENFCE = 'S'";
        if (!empty($f['nfe']))  $w[] = "nc.NFE = 'S'";
        if (!empty($f['so_servicos'])) $w[] = "nc.TOTAL_SERVICOS > 0";

        // Faixas / filtros pontuais
        if ($I($f['nota_ini'] ?? null) !== null || $I($f['nota_fim'] ?? null) !== null) {
            $a = $I($f['nota_ini'] ?? null) ?? 0; $b = $I($f['nota_fim'] ?? null) ?? 2147483647;
            $w[] = "nc.NR_NOTA BETWEEN {$a} AND {$b}";
        }
        if (!empty($f['serie']))    $w[] = "TRIM(nc.SERIE) = " . $S(trim($f['serie']));
        if ($I($f['cli_ini'] ?? null) !== null || $I($f['cli_fim'] ?? null) !== null) {
            $a = $I($f['cli_ini'] ?? null) ?? 0; $b = $I($f['cli_fim'] ?? null) ?? 2147483647;
            $w[] = "nc.COD_PESSOA BETWEEN {$a} AND {$b}";
        }
        if ($I($f['vendedor'] ?? null) !== null) $w[] = "nc.COD_PESSOA_VENDA = " . $I($f['vendedor']);
        if ($I($f['cond'] ?? null) !== null)     $w[] = "nc.COD_COND_PGTO = " . $I($f['cond']);
        if ($I($f['agente'] ?? null) !== null)   $w[] = "nc.COD_AGENTE_VENDA = " . $I($f['agente']);
        if (!empty($f['usuario']))               $w[] = "UPPER(TRIM(nc.USUARIO)) = UPPER(" . $S(trim($f['usuario'])) . ")";
        if ($I($f['forma'] ?? null) !== null) {
            $cf = $I($f['forma']);
            $w[] = "EXISTS (SELECT 1 FROM FORMAS_NOTAS fn WHERE fn.NR_NOTA=nc.NR_NOTA AND fn.SERIE=nc.SERIE AND fn.COD_EMPRESA=nc.COD_EMPRESA AND fn.COD_TIPO_OPERACAO=nc.COD_TIPO_OPERACAO AND fn.COD_FORMA_PGTO={$cf})";
        }
        if (!empty($f['so_promocao'])) {
            $w[] = "EXISTS (SELECT 1 FROM NOTAS_ITENS ni2 WHERE ni2.NR_NOTA=nc.NR_NOTA AND ni2.SERIE=nc.SERIE AND ni2.COD_EMPRESA=nc.COD_EMPRESA AND ni2.COD_TIPO_OPERACAO=nc.COD_TIPO_OPERACAO AND ni2.PROMOCAO='S')";
        }

        $WHERE = implode("\n  AND ", $w);
        $FROM  = "FROM NOTAS_CAB nc JOIN TIPO_OPERACAO t ON t.COD_TIPO_OPERACAO = nc.COD_TIPO_OPERACAO" . ($emp ? " AND t.COD_EMPRESA = nc.COD_EMPRESA" : "");

        $q = function (string $sql) { try { return $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC); } catch (\Throwable $e) { return []; } };
        $utf = function ($s) { return mb_convert_encoding((string)$s, 'UTF-8', 'Windows-1252'); };

        // ── Resumo (sem cap) ──
        $resumo = $q("SELECT COUNT(*) QTD, COALESCE(SUM(nc.TOTAL_NOTA),0) TOTAL, COALESCE(SUM(nc.TOTAL_PRODUTOS),0) PRODUTOS,
                COALESCE(SUM(nc.DESCONTO),0) DESCONTOS, COALESCE(SUM(nc.TOTAL_SERVICOS),0) SERVICOS, COALESCE(SUM(nc.VLR_ICMS),0) ICMS
            {$FROM} WHERE {$WHERE}")[0] ?? ['QTD'=>0,'TOTAL'=>0,'PRODUTOS'=>0,'DESCONTOS'=>0,'SERVICOS'=>0,'ICMS'=>0];

        // ── Custo/lucro (opcional, via itens) ──
        $custoTotal = null;
        if (!empty($f['custos'])) {
            // JOIN a partir das notas filtradas (dirige pela data indexada) — NÃO usar EXISTS sobre todos os itens.
            $r = $q("SELECT COALESCE(SUM(ni.QTD*ni.CUSTO_FINAL),0) CUSTO
                FROM NOTAS_ITENS ni
                JOIN NOTAS_CAB nc ON nc.NR_NOTA=ni.NR_NOTA AND nc.SERIE=ni.SERIE AND nc.COD_EMPRESA=ni.COD_EMPRESA AND nc.COD_TIPO_OPERACAO=ni.COD_TIPO_OPERACAO
                JOIN TIPO_OPERACAO t ON t.COD_TIPO_OPERACAO=nc.COD_TIPO_OPERACAO" . ($emp ? " AND t.COD_EMPRESA=nc.COD_EMPRESA" : "") . "
                WHERE ni.DATA_EMISSAO >= '{$ini}' AND ni.DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$fim}' AS DATE)) AND {$WHERE}");
            $custoTotal = (float)($r[0]['CUSTO'] ?? 0);
        }

        // ── Quebras ──
        $porVendedor = $q("SELECT FIRST 100 TRIM(COALESCE(pv.NOME,'(sem vendedor)')) NOME, COUNT(*) QTD, COALESCE(SUM(nc.TOTAL_NOTA),0) TOTAL
            {$FROM} LEFT JOIN PESSOAS pv ON pv.COD_PESSOA = nc.COD_PESSOA_VENDA
            WHERE {$WHERE} GROUP BY TRIM(COALESCE(pv.NOME,'(sem vendedor)')) ORDER BY 3 DESC");
        $porNatureza = $q("SELECT TRIM(t.DESCRICAO) NOME, COUNT(*) QTD, COALESCE(SUM(nc.TOTAL_NOTA),0) TOTAL
            {$FROM} WHERE {$WHERE} GROUP BY TRIM(t.DESCRICAO) ORDER BY 3 DESC");
        $porSerieRaw = $q("SELECT TRIM(nc.SERIE) SERIE, COUNT(*) QTD, COALESCE(SUM(nc.TOTAL_NOTA),0) TOTAL
            {$FROM} WHERE {$WHERE} GROUP BY TRIM(nc.SERIE) ORDER BY 3 DESC");
        $porForma = $q("SELECT UPPER(TRIM(fp.DESCRICAO)) NOME, COALESCE(SUM(fn.VALOR),0) TOTAL
            FROM FORMAS_NOTAS fn JOIN FORMA_PGTO fp ON fp.COD_FORMA_PGTO = fn.COD_FORMA_PGTO
            JOIN NOTAS_CAB nc ON nc.NR_NOTA=fn.NR_NOTA AND nc.SERIE=fn.SERIE AND nc.COD_EMPRESA=fn.COD_EMPRESA AND nc.COD_TIPO_OPERACAO=fn.COD_TIPO_OPERACAO
            JOIN TIPO_OPERACAO t ON t.COD_TIPO_OPERACAO=nc.COD_TIPO_OPERACAO" . ($emp ? " AND t.COD_EMPRESA=nc.COD_EMPRESA" : "") . "
            WHERE {$WHERE} GROUP BY UPPER(TRIM(fp.DESCRICAO)) ORDER BY 2 DESC");

        foreach ([&$porVendedor,&$porNatureza,&$porForma] as &$lst) { foreach ($lst as &$r) { if(isset($r['NOME'])) $r['NOME']=$utf($r['NOME']); } unset($r); } unset($lst);

        // Série → rótulo amigável (SC=NFCe, numérica/DV=NFe, resto=SN)
        $porSerie = array_map(function ($r) {
            $s = strtoupper(trim((string)$r['SERIE']));
            $lbl = ($s === 'SC') ? 'NFCe' : ((is_numeric($s) || $s === 'DV') ? 'NFe' : 'SN');
            return ['NOME' => $lbl . ' (' . ($s ?: '—') . ')', 'QTD' => $r['QTD'], 'TOTAL' => $r['TOTAL']];
        }, $porSerieRaw);

        // ── Lista de notas (analítico, com cap) ──
        $lim = max(50, min(5000, (int)($f['limite'] ?? 1000)));
        $ord = ['nota'=>'nc.NR_NOTA','data'=>'nc.'.$campoData,'cliente'=>'CLIENTE','vendedor'=>'VENDEDOR','valor'=>'nc.TOTAL_NOTA DESC'][$f['ordenar'] ?? 'data'] ?? 'nc.'.$campoData;
        $custoSel = !empty($f['custos'])
            ? "(SELECT COALESCE(SUM(ni.QTD*ni.CUSTO_FINAL),0) FROM NOTAS_ITENS ni WHERE ni.NR_NOTA=nc.NR_NOTA AND ni.SERIE=nc.SERIE AND ni.COD_EMPRESA=nc.COD_EMPRESA AND ni.COD_TIPO_OPERACAO=nc.COD_TIPO_OPERACAO)"
            : "0";
        $notas = $q("SELECT FIRST {$lim} nc.NR_NOTA, TRIM(nc.SERIE) SERIE, nc.{$campoData} DATA_REF,
                TRIM(COALESCE(pc.NOME, nc.NOME_CONSUMIDOR, '(consumidor)')) CLIENTE,
                TRIM(COALESCE(pv.NOME,'')) VENDEDOR, TRIM(t.DESCRICAO) OPERACAO,
                COALESCE(nc.TOTAL_NOTA,0) TOTAL_NOTA, COALESCE(nc.DESCONTO,0) DESCONTO,
                COALESCE(nc.VLR_ICMS,0) ICMS, nc.COD_SITUACAO, nc.DATA_CANC, {$custoSel} CUSTO
            {$FROM} LEFT JOIN PESSOAS pc ON pc.COD_PESSOA = nc.COD_PESSOA
                    LEFT JOIN PESSOAS pv ON pv.COD_PESSOA = nc.COD_PESSOA_VENDA
            WHERE {$WHERE} ORDER BY {$ord}");
        foreach ($notas as &$r) { $r['CLIENTE']=$utf($r['CLIENTE']); $r['VENDEDOR']=$utf($r['VENDEDOR']); $r['OPERACAO']=$utf($r['OPERACAO']); if(is_object($r['DATA_REF'])) $r['DATA_REF']=$r['DATA_REF']->format('Y-m-d'); } unset($r);

        // ── Produtos (opcional) ──
        $produtos = [];
        if (!empty($f['produtos'])) {
            $produtos = $q("SELECT FIRST 1000 ni.COD_ITEM, TRIM(MAX(ni.DESCRICAO)) DESCRICAO,
                    COALESCE(SUM(ni.QTD),0) QTD, COALESCE(SUM(ni.QTD*ni.PRECO_UNIT),0) TOTAL, COALESCE(SUM(ni.QTD*ni.CUSTO_FINAL),0) CUSTO
                FROM NOTAS_ITENS ni
                JOIN NOTAS_CAB nc ON nc.NR_NOTA=ni.NR_NOTA AND nc.SERIE=ni.SERIE AND nc.COD_EMPRESA=ni.COD_EMPRESA AND nc.COD_TIPO_OPERACAO=ni.COD_TIPO_OPERACAO
                JOIN TIPO_OPERACAO t ON t.COD_TIPO_OPERACAO=nc.COD_TIPO_OPERACAO" . ($emp ? " AND t.COD_EMPRESA=nc.COD_EMPRESA" : "") . "
                WHERE ni.DATA_EMISSAO >= '{$ini}' AND ni.DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$fim}' AS DATE)) AND {$WHERE}
                GROUP BY ni.COD_ITEM ORDER BY 4 DESC");
            foreach ($produtos as &$r) { $r['DESCRICAO']=$utf($r['DESCRICAO']); } unset($r);
        }

        $out = [
            'resumo'       => $resumo,
            'custo_total'  => $custoTotal,
            'por_vendedor' => $porVendedor,
            'por_natureza' => $porNatureza,
            'por_serie'    => $porSerie,
            'por_forma'    => $porForma,
            'notas'        => $notas,
            'produtos'     => $produtos,
            'capado'       => count($notas) >= $lim,
            'limite'       => $lim,
            'periodo'      => ['ini'=>$ini, 'fim'=>$fim, 'campo'=>$campoData],
        ];
        \App\Core\Cache::set($ck, $out, 120);
        return $out;
    }

    // -------------------------------------------------------------------------
    // Faturamento fiscal por série
    // -------------------------------------------------------------------------

    public function faturamentoFiscal(string $dataIni, string $dataFim, ?int $codVendedor = null): array
    {
        $filtroVend = $codVendedor ? "AND COD_PESSOA_VENDA = {$codVendedor}" : '';
        // _v2_: classificação por FLAGS reais (ENFCE/NFE), não mais pela string da SERIE.
        $cacheKey   = "vendas_fiscal_v2_{$dataIni}_{$dataFim}_" . ($codVendedor ?? '0');

        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $filtroVend) {
            // Documento fiscal pelo CADASTRO da nota (campos da NOTAS_CAB do ERP Master):
            //   ENFCE = 'S' -> NFC-e (modelo 65, cupom ao consumidor)
            //   NFE   = 'S' -> NF-e  (modelo 55, nota)
            //   nenhum dos dois -> SN (venda sem nota fiscal)
            // Restrito a VENDAS (filtroVenda exclui compras/devoluções/transferências/mov. interna).
            try {
                // TRIM externo: o Firebird preenche o resultado do CASE com espaços até o
                // tamanho do maior literal ('NFCe (65)'=9), o que deixaria 'SN'->'SN       '.
                $sql = "
                    SELECT
                        TRIM(CASE
                            WHEN UPPER(TRIM(COALESCE(ENFCE, ''))) = 'S' THEN 'NFCe (65)'
                            WHEN UPPER(TRIM(COALESCE(NFE,   ''))) = 'S' THEN 'NFe (55)'
                            ELSE 'SN'
                        END)                              AS DOC,
                        COUNT(*)                          AS QTD,
                        COALESCE(SUM(TOTAL_NOTA), 0)      AS TOTAL
                    FROM NOTAS_CAB
                    WHERE {$this->intervaloData($dataIni, $dataFim, '')}
                      AND DATA_CANC IS NULL
                      AND TOTAL_NOTA > 0
                      {$this->filtroVenda('')}
                      {$this->filtroEmpresa('')}
                      {$filtroVend}
                    GROUP BY 1
                    ORDER BY 3 DESC
                ";
                $rows = $this->pdo->query($sql)->fetchAll();
                $out = [];
                foreach ($rows as $r) {
                    $out[] = ['label' => (string)$r['DOC'], 'qtd' => (int)$r['QTD'], 'total' => (float)$r['TOTAL']];
                }
                return $out;
            } catch (\Throwable) {
                // Fallback (bancos sem coluna ENFCE/NFE): classifica pela SERIE como antes.
                try {
                    $rows = $this->pdo->query("
                        SELECT TRIM(SERIE) AS SERIE, COUNT(*) AS QTD, COALESCE(SUM(TOTAL_NOTA),0) AS TOTAL
                        FROM NOTAS_CAB
                        WHERE {$this->intervaloData($dataIni, $dataFim, '')}
                          AND DATA_CANC IS NULL AND TOTAL_NOTA > 0
                          {$this->filtroVenda('')} {$this->filtroEmpresa('')} {$filtroVend}
                        GROUP BY TRIM(SERIE) ORDER BY TOTAL DESC
                    ")->fetchAll();
                    $mapa = [];
                    foreach ($rows as $r) {
                        $serie = strtoupper(trim((string)$r['SERIE']));
                        if ($serie === 'SC')                              $chave = 'NFCe (65)';
                        elseif (is_numeric($serie) || $serie === 'DV')    $chave = 'NFe (55)';
                        else                                              $chave = 'SN';
                        if (!isset($mapa[$chave])) $mapa[$chave] = ['label' => $chave, 'qtd' => 0, 'total' => 0.0];
                        $mapa[$chave]['qtd']   += (int)$r['QTD'];
                        $mapa[$chave]['total'] += (float)$r['TOTAL'];
                    }
                    return array_values($mapa);
                } catch (\Throwable) { return []; }
            }
        });
    }

    // -------------------------------------------------------------------------
    // Vendas por dia da semana
    // -------------------------------------------------------------------------

    public function vendasPorDiaSemana(string $dataIni, string $dataFim, ?int $codVendedor = null): array
    {
        $filtroVend = $codVendedor ? "AND COD_PESSOA_VENDA = {$codVendedor}" : '';
        $cacheKey   = "vendas_diasemana_{$dataIni}_{$dataFim}_" . ($codVendedor ?? '0');

        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $filtroVend) {
            try {
                $rows = $this->pdo->query("
                    SELECT
                        EXTRACT(WEEKDAY FROM DATA_EMISSAO) AS DIA_NUM,
                        COUNT(*)                            AS QTD,
                        COALESCE(SUM(TOTAL_NOTA), 0)        AS TOTAL
                    FROM NOTAS_CAB
                    WHERE {$this->intervaloData($dataIni, $dataFim, '')}
                      AND DATA_CANC IS NULL
                      AND TOTAL_NOTA > 0
                      {$this->filtroVenda('')}
                      {$this->filtroEmpresa('')}
                      {$filtroVend}
                    GROUP BY EXTRACT(WEEKDAY FROM DATA_EMISSAO)
                    ORDER BY DIA_NUM
                ")->fetchAll();

                $nomes = ['Domingo','Segunda','Terça','Quarta','Quinta','Sexta','Sábado'];
                $mapa  = array_fill(0, 7, ['dia' => '', 'qtd' => 0, 'total' => 0.0]);
                foreach ($nomes as $i => $n) $mapa[$i]['dia'] = $n;
                foreach ($rows as $r) {
                    $idx = (int)$r['DIA_NUM'];
                    if ($idx >= 0 && $idx <= 6) {
                        $mapa[$idx]['qtd']   = (int)$r['QTD'];
                        $mapa[$idx]['total'] = round((float)$r['TOTAL'], 2);
                    }
                }
                return $mapa;
            } catch (\Throwable) { return []; }
        });
    }

    // -------------------------------------------------------------------------
    // Lista de vendedores (para o filtro)
    // -------------------------------------------------------------------------

    public function listaVendedores(): array
    {
        // TTL 24h — operadoras mudam raramente
        return Cache::lembrar('vendas_lista_vendedores', 86400, function () {
            // Tentativa 1: operadoras de caixa (ABERTURA_CAIXA)
            try {
                $this->filtroEmpresa('');
                $feCond = self::$codEmpresa ? "AND ac.COD_EMPRESA = " . self::$codEmpresa : '';
                $ini = date('Y-m-d', strtotime('-12 months'));
                $rows = $this->pdo->query("
                    SELECT FIRST 100
                        ac.COD_USUARIO_ABERTURA                        AS COD_PESSOA_VENDA,
                        TRIM(COALESCE(u.NOME, ac.COD_USUARIO_ABERTURA)) AS NOME
                    FROM (
                        SELECT DISTINCT COD_USUARIO_ABERTURA, COD_EMPRESA
                        FROM ABERTURA_CAIXA
                        WHERE DATA_ABERTURA >= '{$ini}'
                          AND COD_USUARIO_ABERTURA IS NOT NULL
                          {$feCond}
                    ) ac
                    LEFT JOIN USUARIOS u ON u.COD_USUARIO = ac.COD_USUARIO_ABERTURA
                                        AND u.COD_EMPRESA  = ac.COD_EMPRESA
                    ORDER BY TRIM(COALESCE(u.NOME, ac.COD_USUARIO_ABERTURA))
                ")->fetchAll();
                if (!empty($rows)) return $this->utf8Nomes($rows);
            } catch (\Throwable) {}

            // Tentativa 2: garçons ativos (restaurantes)
            try {
                $rows = $this->pdo->query("
                    SELECT FIRST 100
                        g.COD_GARCOM AS COD_PESSOA_VENDA,
                        TRIM(p.NOME) AS NOME
                    FROM GARCOM g
                    JOIN PESSOAS p ON p.COD_PESSOA = g.COD_PESSOA
                    WHERE g.ATIVO = 'S'
                    ORDER BY p.NOME
                ")->fetchAll();
                if (!empty($rows)) return $this->utf8Nomes($rows);
            } catch (\Throwable) {}

            // Tentativa 3: COD_PESSOA_VENDA distintos nos últimos 12 meses
            try {
                $this->filtroEmpresa('');
                $feCond = self::$codEmpresa ? "AND COD_EMPRESA = " . self::$codEmpresa : '';
                $ini = date('Y-m-d', strtotime('-12 months'));
                return $this->pdo->query("
                    SELECT FIRST 100
                        nc.COD_PESSOA_VENDA,
                        TRIM(p.NOME) AS NOME
                    FROM (
                        SELECT DISTINCT COD_PESSOA_VENDA
                        FROM NOTAS_CAB
                        WHERE DATA_EMISSAO >= '{$ini}'
                          AND DATA_CANC IS NULL
                          AND COD_PESSOA_VENDA IS NOT NULL
                          {$feCond}
                    ) nc
                    JOIN PESSOAS p ON p.COD_PESSOA = nc.COD_PESSOA_VENDA
                    ORDER BY p.NOME
                ")->fetchAll();
            } catch (\Throwable) {
                return [];
            }
        });
    }

    public function listaUsuarios(): array
    {
        return Cache::lembrar('vendas_lista_usuarios', 1800, function () {
            $ini = date('Y-m-d', strtotime('-12 months'));
            $tentativas = [
                "SELECT DISTINCT FIRST 100 TRIM(COD_USUARIO) AS USUARIO FROM NOTAS_CAB
                 WHERE DATA_EMISSAO >= '{$ini}' AND DATA_CANC IS NULL AND COD_USUARIO IS NOT NULL
                 ORDER BY USUARIO",
                "SELECT DISTINCT FIRST 100 TRIM(USUARIO) AS USUARIO FROM NOTAS_CAB
                 WHERE DATA_EMISSAO >= '{$ini}' AND DATA_CANC IS NULL AND USUARIO IS NOT NULL
                 ORDER BY USUARIO",
                "SELECT DISTINCT FIRST 100 TRIM(LOGIN) AS USUARIO FROM NOTAS_CAB
                 WHERE DATA_EMISSAO >= '{$ini}' AND DATA_CANC IS NULL AND LOGIN IS NOT NULL
                 ORDER BY USUARIO",
            ];
            foreach ($tentativas as $sql) {
                try {
                    $rows = $this->pdo->query($sql)->fetchAll();
                    if (!empty($rows)) return $rows;
                } catch (\Throwable) {}
            }
            return [];
        });
    }

    // -------------------------------------------------------------------------
    // Vendas diárias (tabela detalhada)
    // -------------------------------------------------------------------------

    public function vendasDiarias(string $dataIni, string $dataFim, ?int $codVendedor = null): array
    {
        $filtroVend = $codVendedor ? "AND nc.COD_PESSOA_VENDA = {$codVendedor}" : '';
        $cacheKey   = "vendas_diarias_{$dataIni}_{$dataFim}_" . ($codVendedor ?? '0');

        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $filtroVend) {
            $fv = $this->filtroVenda('nc');
            $fe = $this->filtroEmpresa('nc');
            $iv = $this->intervaloData($dataIni, $dataFim, 'nc');

            // Tentativa 1: com QTD_ITENS filtrado por data (evita full scan de NOTAS_ITENS)
            try {
                $rows = $this->pdo->query("
                    SELECT
                        nc.DATA_EMISSAO                           AS DATA,
                        COUNT(*)                                   AS QTD_NOTAS,
                        COALESCE(SUM(nc.TOTAL_NOTA), 0)           AS TOTAL,
                        COALESCE(AVG(nc.TOTAL_NOTA), 0)           AS TICKET_MEDIO,
                        COALESCE(SUM(ni_qtd.QTD_ITENS), 0)        AS QTD_ITENS
                    FROM NOTAS_CAB nc
                    LEFT JOIN (
                        SELECT ni.NR_NOTA, ni.SERIE, ni.COD_EMPRESA, ni.COD_TIPO_OPERACAO,
                               SUM(ni.QTD) AS QTD_ITENS
                        FROM NOTAS_ITENS ni
                        WHERE {$this->intervaloData($dataIni, $dataFim, 'ni')}
                        GROUP BY ni.NR_NOTA, ni.SERIE, ni.COD_EMPRESA, ni.COD_TIPO_OPERACAO
                    ) ni_qtd ON ni_qtd.NR_NOTA           = nc.NR_NOTA
                             AND ni_qtd.SERIE             = nc.SERIE
                             AND ni_qtd.COD_EMPRESA       = nc.COD_EMPRESA
                             AND ni_qtd.COD_TIPO_OPERACAO = nc.COD_TIPO_OPERACAO
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL
                      {$fv}
                      {$fe}
                      {$filtroVend}
                    GROUP BY nc.DATA_EMISSAO
                    ORDER BY nc.DATA_EMISSAO DESC
                ")->fetchAll(\PDO::FETCH_ASSOC);
                if ($rows !== false) return $rows;
            } catch (\Throwable) {}

            // Tentativa 2: sem JOIN em NOTAS_ITENS (fallback seguro, QTD_ITENS = 0)
            try {
                return $this->pdo->query("
                    SELECT
                        nc.DATA_EMISSAO                  AS DATA,
                        COUNT(*)                          AS QTD_NOTAS,
                        COALESCE(SUM(nc.TOTAL_NOTA), 0)  AS TOTAL,
                        COALESCE(AVG(nc.TOTAL_NOTA), 0)  AS TICKET_MEDIO,
                        0                                 AS QTD_ITENS
                    FROM NOTAS_CAB nc
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL
                      {$fv}
                      {$fe}
                      {$filtroVend}
                    GROUP BY nc.DATA_EMISSAO
                    ORDER BY nc.DATA_EMISSAO DESC
                ")->fetchAll(\PDO::FETCH_ASSOC);
            } catch (\Throwable) { return []; }
        });
    }

    // -------------------------------------------------------------------------
    // Faturamento mensal — usa BETWEEN em vez de EXTRACT(YEAR) para usar índice
    // -------------------------------------------------------------------------

    public function vendasMensais(int $ano, ?int $codVendedor = null): array
    {
        $filtroVend = $codVendedor ? "AND nc.COD_PESSOA_VENDA = {$codVendedor}" : '';
        $cacheKey   = "vendas_mensais_{$ano}_" . ($codVendedor ?? '0');
        $ttl        = ($ano === (int)date('Y')) ? 300 : 1800;

        return Cache::lembrar($cacheKey, $ttl, function () use ($ano, $filtroVend) {
            $ini = "{$ano}-01-01";
            $fim = "{$ano}-12-31";
            return $this->pdo->query("
                SELECT
                    EXTRACT(MONTH FROM nc.DATA_EMISSAO) AS MES,
                    COUNT(*)                             AS QTD_NOTAS,
                    COALESCE(SUM(nc.TOTAL_NOTA), 0)     AS TOTAL,
                    COALESCE(AVG(nc.TOTAL_NOTA), 0)     AS TICKET_MEDIO
                FROM NOTAS_CAB nc
                WHERE {$this->intervaloData($ini, $fim, 'nc')}
                  AND nc.DATA_CANC IS NULL
                  {$this->filtroVenda('nc')}
                  {$this->filtroEmpresa('nc')}
                  {$filtroVend}
                GROUP BY EXTRACT(MONTH FROM nc.DATA_EMISSAO)
                ORDER BY MES
            ")->fetchAll();
        });
    }

    // -------------------------------------------------------------------------
    // Vendas por vendedor
    // -------------------------------------------------------------------------

    /**
     * Converte campos de texto (nomes vindos do banco em WIN1252) p/ UTF-8.
     * Sem isso, um nome com acento (JOÃO, ANDRÉ, Garçom…) faz o json_encode
     * retornar false → corpo vazio → "Unexpected end of JSON input" na tela.
     */
    private function utf8Nomes(array $rows): array
    {
        foreach ($rows as &$r) {
            if (!is_array($r)) continue;
            foreach (['VENDEDOR', 'CLIENTE', 'NOME', 'DESCRICAO'] as $k) {
                if (isset($r[$k]) && $r[$k] !== '' && !mb_check_encoding((string)$r[$k], 'UTF-8')) {
                    $r[$k] = mb_convert_encoding((string)$r[$k], 'UTF-8', 'Windows-1252');
                }
            }
        }
        unset($r);
        return $rows;
    }

    public function vendasPorVendedor(string $dataIni, string $dataFim): array
    {
        $cacheKey = "vendas_por_vendedor_{$dataIni}_{$dataFim}";

        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            $iv = $this->intervaloData($dataIni, $dataFim, 'nc');
            $fe = $this->filtroEmpresa('nc');
            $feAc = $this->filtroEmpresa('ac');
            $fv = $this->filtroVenda('nc');

            // Tentativa 1: operadoras de caixa — ABERTURA_CAIXA via COD_ABERT_CAIXA
            // COD_USUARIO_ABERTURA armazena o nome/login da operadora diretamente
            try {
                $rows = $this->pdo->query("
                    SELECT FIRST 50
                        ac.COD_USUARIO_ABERTURA                                              AS COD_VENDEDOR,
                        TRIM(COALESCE(u.NOME, ac.COD_USUARIO_ABERTURA))                     AS VENDEDOR,
                        COUNT(*)                         AS QTD_NOTAS,
                        COALESCE(SUM(nc.TOTAL_NOTA), 0)  AS TOTAL,
                        COALESCE(AVG(nc.TOTAL_NOTA), 0)  AS TICKET_MEDIO
                    FROM NOTAS_CAB nc
                    JOIN ABERTURA_CAIXA ac ON ac.COD_ABERT_CAIXA = nc.COD_ABERT_CAIXA
                    LEFT JOIN USUARIOS u   ON u.COD_USUARIO = ac.COD_USUARIO_ABERTURA
                                         AND u.COD_EMPRESA  = ac.COD_EMPRESA
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      AND ac.COD_USUARIO_ABERTURA IS NOT NULL
                      {$fv}
                      {$fe}
                    GROUP BY ac.COD_USUARIO_ABERTURA,
                             TRIM(COALESCE(u.NOME, ac.COD_USUARIO_ABERTURA))
                    ORDER BY TOTAL DESC
                ")->fetchAll();
                if (!empty($rows)) return $this->utf8Nomes($rows);
            } catch (\Throwable) {}

            // Tentativa 2: restaurantes — via ABERT_MESA + GARCOM (mesas/garçons)
            try {
                $rows = $this->pdo->query("
                    SELECT FIRST 50
                        g.COD_GARCOM                                                            AS COD_VENDEDOR,
                        TRIM(COALESCE(p.NOME, 'Garçom ' || CAST(g.COD_GARCOM AS VARCHAR(20)))) AS VENDEDOR,
                        COUNT(*)                         AS QTD_NOTAS,
                        COALESCE(SUM(nc.TOTAL_NOTA), 0)  AS TOTAL,
                        COALESCE(AVG(nc.TOTAL_NOTA), 0)  AS TICKET_MEDIO
                    FROM NOTAS_CAB nc
                    JOIN ABERT_MESA am  ON am.COD_ABERTURA = nc.COD_ABERTURA
                    JOIN GARCOM g       ON g.COD_GARCOM    = am.COD_GARCOM
                    LEFT JOIN PESSOAS p ON p.COD_PESSOA    = g.COD_PESSOA
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      AND nc.COD_ABERTURA IS NOT NULL
                      {$fe}
                    GROUP BY g.COD_GARCOM,
                             TRIM(COALESCE(p.NOME, 'Garçom ' || CAST(g.COD_GARCOM AS VARCHAR(20))))
                    ORDER BY TOTAL DESC
                ")->fetchAll();
                if (!empty($rows)) return $this->utf8Nomes($rows);
            } catch (\Throwable) {}

            // Tentativa 3: COD_PESSOA_REPRE (representante de vendas — ERPs de distribuição)
            try {
                $rows = $this->pdo->query("
                    SELECT FIRST 50
                        nc.COD_PESSOA_REPRE                                                             AS COD_VENDEDOR,
                        TRIM(COALESCE(p.NOME, 'Rep. ' || CAST(nc.COD_PESSOA_REPRE AS VARCHAR(20))))    AS VENDEDOR,
                        COUNT(*)                         AS QTD_NOTAS,
                        COALESCE(SUM(nc.TOTAL_NOTA), 0)  AS TOTAL,
                        COALESCE(AVG(nc.TOTAL_NOTA), 0)  AS TICKET_MEDIO
                    FROM NOTAS_CAB nc
                    LEFT JOIN PESSOAS p ON p.COD_PESSOA = nc.COD_PESSOA_REPRE
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      AND nc.COD_PESSOA_REPRE IS NOT NULL
                      {$fv}
                      {$fe}
                    GROUP BY nc.COD_PESSOA_REPRE,
                             TRIM(COALESCE(p.NOME, 'Rep. ' || CAST(nc.COD_PESSOA_REPRE AS VARCHAR(20))))
                    ORDER BY TOTAL DESC
                ")->fetchAll();
                if (!empty($rows)) return $this->utf8Nomes($rows);
            } catch (\Throwable) {}

            // Tentativa 4: COD_PESSOA_VENDA (fallback genérico)
            try {
                $rows = $this->pdo->query("
                    SELECT FIRST 50
                        nc.COD_PESSOA_VENDA                                                              AS COD_VENDEDOR,
                        TRIM(COALESCE(p.NOME, 'Vendedor ' || CAST(nc.COD_PESSOA_VENDA AS VARCHAR(20)))) AS VENDEDOR,
                        COUNT(*)                         AS QTD_NOTAS,
                        COALESCE(SUM(nc.TOTAL_NOTA), 0)  AS TOTAL,
                        COALESCE(AVG(nc.TOTAL_NOTA), 0)  AS TICKET_MEDIO
                    FROM NOTAS_CAB nc
                    LEFT JOIN PESSOAS p ON p.COD_PESSOA = nc.COD_PESSOA_VENDA
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      AND nc.COD_PESSOA_VENDA IS NOT NULL
                      {$fv}
                      {$fe}
                    GROUP BY nc.COD_PESSOA_VENDA,
                             TRIM(COALESCE(p.NOME, 'Vendedor ' || CAST(nc.COD_PESSOA_VENDA AS VARCHAR(20))))
                    ORDER BY TOTAL DESC
                ")->fetchAll();
                return $this->utf8Nomes($rows);
            } catch (\Throwable) { return []; }
        });
    }

    // =========================================================================
    // VENDAS POR VENDEDOR — métricas extras (v1.4.3+)
    // Endpoints ADICIONAIS, não modificam vendasPorVendedor() acima.
    // =========================================================================

    /**
     * Detecta qual fonte de vendedor o cliente usa e retorna a config SQL pronta
     * pra montar queries derivadas. Idêntico em estratégia ao vendasPorVendedor()
     * (4 tentativas: caixa → garçom → representante → pessoa_venda), mas devolve
     * a FONTE detectada + JOIN + coluna do vendedor. Cacheado por período.
     */
    private function fonteVendedor(string $dataIni, string $dataFim): ?array
    {
        $cacheKey = "vend_fonte_{$dataIni}_{$dataFim}_" . ($this->codEmpresa ?? 'na');
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            $iv = $this->intervaloData($dataIni, $dataFim, 'nc');
            $fe = $this->filtroEmpresa('nc');
            $fv = $this->filtroVenda('nc');
            $candidatos = [
                [
                    'tipo'   => 'caixa',
                    'join'   => "JOIN ABERTURA_CAIXA ac ON ac.COD_ABERT_CAIXA = nc.COD_ABERT_CAIXA "
                              . "LEFT JOIN USUARIOS u ON u.COD_USUARIO = ac.COD_USUARIO_ABERTURA AND u.COD_EMPRESA = ac.COD_EMPRESA",
                    'col'    => 'ac.COD_USUARIO_ABERTURA',
                    'nome'   => "TRIM(COALESCE(u.NOME, ac.COD_USUARIO_ABERTURA))",
                    'where'  => "ac.COD_USUARIO_ABERTURA IS NOT NULL",
                    'usa_fv' => true,
                ],
                [
                    'tipo'   => 'garcom',
                    'join'   => "JOIN ABERT_MESA am ON am.COD_ABERTURA = nc.COD_ABERTURA "
                              . "JOIN GARCOM g ON g.COD_GARCOM = am.COD_GARCOM "
                              . "LEFT JOIN PESSOAS p ON p.COD_PESSOA = g.COD_PESSOA",
                    'col'    => 'g.COD_GARCOM',
                    'nome'   => "TRIM(COALESCE(p.NOME, 'Garçom ' || CAST(g.COD_GARCOM AS VARCHAR(20))))",
                    'where'  => "nc.COD_ABERTURA IS NOT NULL",
                    'usa_fv' => false,
                ],
                [
                    'tipo'   => 'repre',
                    'join'   => "LEFT JOIN PESSOAS p ON p.COD_PESSOA = nc.COD_PESSOA_REPRE",
                    'col'    => 'nc.COD_PESSOA_REPRE',
                    'nome'   => "TRIM(COALESCE(p.NOME, 'Rep. ' || CAST(nc.COD_PESSOA_REPRE AS VARCHAR(20))))",
                    'where'  => "nc.COD_PESSOA_REPRE IS NOT NULL",
                    'usa_fv' => true,
                ],
                [
                    'tipo'   => 'venda',
                    'join'   => "LEFT JOIN PESSOAS p ON p.COD_PESSOA = nc.COD_PESSOA_VENDA",
                    'col'    => 'nc.COD_PESSOA_VENDA',
                    'nome'   => "TRIM(COALESCE(p.NOME, 'Vendedor ' || CAST(nc.COD_PESSOA_VENDA AS VARCHAR(20))))",
                    'where'  => "nc.COD_PESSOA_VENDA IS NOT NULL",
                    'usa_fv' => true,
                ],
            ];
            foreach ($candidatos as $c) {
                try {
                    $extraFv = $c['usa_fv'] ? $this->filtroVenda('nc') : '';
                    $cnt = (int)$this->pdo->query("
                        SELECT COUNT(*) FROM NOTAS_CAB nc
                        {$c['join']}
                        WHERE {$iv}
                          AND nc.DATA_CANC IS NULL AND nc.TOTAL_NOTA > 0
                          AND {$c['where']}
                          {$extraFv}
                          {$fe}
                    ")->fetchColumn();
                    if ($cnt > 0) return $c;
                } catch (\Throwable) {}
            }
            return null;
        });
    }

    /**
     * Métricas extras: equipe, gap top1×último, evolução vs período anterior, dias com venda.
     */
    public function vendedorMetricasGerais(string $dataIni, string $dataFim): array
    {
        $cacheKey = "vend_metricas_{$dataIni}_{$dataFim}_" . ($this->codEmpresa ?? 'na');
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            $linhas = $this->vendasPorVendedor($dataIni, $dataFim);
            if (!$linhas) return ['equipe' => 0, 'top1' => null, 'gap_pp' => null, 'evol_pct' => null, 'fonte' => null];

            $totFat = array_sum(array_map(fn($l) => (float)$l['TOTAL'], $linhas));
            $top1 = $linhas[0];
            $top1Pct = $totFat > 0 ? ((float)$top1['TOTAL']) / $totFat * 100 : 0;
            $ultimo = end($linhas);
            $ultPct = $totFat > 0 ? ((float)$ultimo['TOTAL']) / $totFat * 100 : 0;

            // Faturamento do período anterior (mesma quantidade de dias)
            $diasPeriodo = max(1, (strtotime($dataFim) - strtotime($dataIni)) / 86400 + 1);
            $fimAnt = date('Y-m-d', strtotime($dataIni . ' -1 day'));
            $iniAnt = date('Y-m-d', strtotime($fimAnt . ' -' . ((int)$diasPeriodo - 1) . ' days'));
            $linhasAnt = $this->vendasPorVendedor($iniAnt, $fimAnt);
            $totAnt = array_sum(array_map(fn($l) => (float)$l['TOTAL'], $linhasAnt));
            $evolPct = $totAnt > 0 ? ($totFat - $totAnt) / $totAnt * 100 : null;

            return [
                'equipe'   => count($linhas),
                'top1'     => ['nome' => $top1['VENDEDOR'], 'total' => (float)$top1['TOTAL'], 'pct' => $top1Pct, 'notas' => (int)$top1['QTD_NOTAS']],
                'gap_pp'   => $top1Pct - $ultPct,
                'evol_pct' => $evolPct,
                'tot_fat'  => $totFat,
                'tot_ant'  => $totAnt,
                'ini_ant'  => $iniAnt,
                'fim_ant'  => $fimAnt,
            ];
        });
    }

    /**
     * Evolução diária (faturamento por dia) dos TOP N vendedores no período.
     * Retorna ['datas' => [Y-m-d, ...], 'series' => [['nome', 'valores' => [...]], ...]]
     */
    public function vendedorEvolucaoTopN(string $dataIni, string $dataFim, int $topN = 5): array
    {
        $topN = max(1, min(10, $topN));
        $cacheKey = "vend_evol_{$dataIni}_{$dataFim}_{$topN}_" . ($this->codEmpresa ?? 'na');
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $topN) {
            $fonte = $this->fonteVendedor($dataIni, $dataFim);
            if (!$fonte) return ['datas' => [], 'series' => []];
            $topLinhas = array_slice($this->vendasPorVendedor($dataIni, $dataFim), 0, $topN);
            if (!$topLinhas) return ['datas' => [], 'series' => []];

            // Lista de datas do período
            $datas = [];
            for ($d = strtotime($dataIni); $d <= strtotime($dataFim); $d += 86400) {
                $datas[] = date('Y-m-d', $d);
            }

            $iv = $this->intervaloData($dataIni, $dataFim, 'nc');
            $fe = $this->filtroEmpresa('nc');
            $fv = $fonte['usa_fv'] ? $this->filtroVenda('nc') : '';

            // Mapeia COD_VENDEDOR → nome do top N
            $codsTop = array_column($topLinhas, 'COD_VENDEDOR');
            $nomesTop = array_combine($codsTop, array_column($topLinhas, 'VENDEDOR'));

            // Escape SQL pros códigos (podem ser string como "LAURA CX" ou int)
            $codsEsc = array_map(function ($c) {
                return "'" . str_replace("'", "''", (string)$c) . "'";
            }, $codsTop);
            $inList = implode(',', $codsEsc);

            try {
                $sql = "
                    SELECT
                        {$fonte['col']} AS COD_V,
                        CAST(nc.DATA_EMISSAO AS DATE) AS DIA,
                        COALESCE(SUM(nc.TOTAL_NOTA), 0) AS TOTAL
                    FROM NOTAS_CAB nc
                    {$fonte['join']}
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL AND nc.TOTAL_NOTA > 0
                      AND {$fonte['where']}
                      AND CAST({$fonte['col']} AS VARCHAR(50)) IN ({$inList})
                      {$fv}
                      {$fe}
                    GROUP BY {$fonte['col']}, CAST(nc.DATA_EMISSAO AS DATE)
                ";
                $rows = $this->pdo->query($sql)->fetchAll();
            } catch (\Throwable) {
                return ['datas' => $datas, 'series' => []];
            }

            // Indexa: cod → [data → total]
            $idx = [];
            foreach ($rows as $r) {
                $cod = (string)$r['COD_V'];
                $dia = $r['DIA'];
                if (!isset($idx[$cod])) $idx[$cod] = [];
                $idx[$cod][$dia] = (float)$r['TOTAL'];
            }

            // Monta series na ORDEM do top N
            $series = [];
            foreach ($topLinhas as $l) {
                $cod = (string)$l['COD_VENDEDOR'];
                $valores = [];
                foreach ($datas as $d) $valores[] = round($idx[$cod][$d] ?? 0, 2);
                $series[] = ['nome' => $l['VENDEDOR'], 'valores' => $valores];
            }
            return ['datas' => $datas, 'series' => $series];
        });
    }

    /**
     * Heatmap: top N vendedores × dia da semana (0=domingo … 6=sábado).
     * Retorna ['vendedores' => [nomes...], 'matriz' => [[0,0,0,0,0,0,0], ...]]
     */
    public function vendedorDiaSemana(string $dataIni, string $dataFim, int $topN = 8): array
    {
        $topN = max(1, min(15, $topN));
        $cacheKey = "vend_dsem_{$dataIni}_{$dataFim}_{$topN}_" . ($this->codEmpresa ?? 'na');
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $topN) {
            $fonte = $this->fonteVendedor($dataIni, $dataFim);
            if (!$fonte) return ['vendedores' => [], 'matriz' => []];
            $topLinhas = array_slice($this->vendasPorVendedor($dataIni, $dataFim), 0, $topN);
            if (!$topLinhas) return ['vendedores' => [], 'matriz' => []];

            $iv = $this->intervaloData($dataIni, $dataFim, 'nc');
            $fe = $this->filtroEmpresa('nc');
            $fv = $fonte['usa_fv'] ? $this->filtroVenda('nc') : '';

            $codsTop = array_column($topLinhas, 'COD_VENDEDOR');
            $codsEsc = array_map(fn($c) => "'" . str_replace("'", "''", (string)$c) . "'", $codsTop);
            $inList = implode(',', $codsEsc);

            try {
                // Firebird: EXTRACT(WEEKDAY FROM data) → 0=domingo
                $sql = "
                    SELECT
                        {$fonte['col']} AS COD_V,
                        EXTRACT(WEEKDAY FROM nc.DATA_EMISSAO) AS DSEM,
                        COALESCE(SUM(nc.TOTAL_NOTA), 0) AS TOTAL
                    FROM NOTAS_CAB nc
                    {$fonte['join']}
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL AND nc.TOTAL_NOTA > 0
                      AND {$fonte['where']}
                      AND CAST({$fonte['col']} AS VARCHAR(50)) IN ({$inList})
                      {$fv}
                      {$fe}
                    GROUP BY {$fonte['col']}, EXTRACT(WEEKDAY FROM nc.DATA_EMISSAO)
                ";
                $rows = $this->pdo->query($sql)->fetchAll();
            } catch (\Throwable) {
                return ['vendedores' => array_column($topLinhas, 'VENDEDOR'), 'matriz' => []];
            }

            $idx = [];
            foreach ($rows as $r) {
                $cod = (string)$r['COD_V'];
                $ds  = (int)$r['DSEM'];
                if (!isset($idx[$cod])) $idx[$cod] = array_fill(0, 7, 0);
                $idx[$cod][$ds] = (float)$r['TOTAL'];
            }

            $nomes  = [];
            $matriz = [];
            foreach ($topLinhas as $l) {
                $cod = (string)$l['COD_VENDEDOR'];
                $nomes[]  = $l['VENDEDOR'];
                $matriz[] = $idx[$cod] ?? array_fill(0, 7, 0);
            }
            return ['vendedores' => $nomes, 'matriz' => $matriz];
        });
    }

    /**
     * Top N vendedores com métricas aprofundadas:
     *   clientes únicos, devoluções (qtd e valor), primeira/última venda, dias ativos.
     */
    public function vendedorAprofundado(string $dataIni, string $dataFim, int $topN = 10): array
    {
        $topN = max(1, min(20, $topN));
        $cacheKey = "vend_aprof_{$dataIni}_{$dataFim}_{$topN}_" . ($this->codEmpresa ?? 'na');
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $topN) {
            $fonte = $this->fonteVendedor($dataIni, $dataFim);
            if (!$fonte) return [];
            $topLinhas = array_slice($this->vendasPorVendedor($dataIni, $dataFim), 0, $topN);
            if (!$topLinhas) return [];

            $iv  = $this->intervaloData($dataIni, $dataFim, 'nc');
            $fe  = $this->filtroEmpresa('nc');
            $fv  = $fonte['usa_fv'] ? $this->filtroVenda('nc') : '';

            $codsTop = array_column($topLinhas, 'COD_VENDEDOR');
            $codsEsc = array_map(fn($c) => "'" . str_replace("'", "''", (string)$c) . "'", $codsTop);
            $inList = implode(',', $codsEsc);

            // Vendas (não canceladas, total>0)
            try {
                $sql = "
                    SELECT
                        {$fonte['col']} AS COD_V,
                        COUNT(DISTINCT nc.COD_PESSOA) AS CLIENTES,
                        MIN(nc.DATA_EMISSAO) AS PRIMEIRA,
                        MAX(nc.DATA_EMISSAO) AS ULTIMA,
                        COUNT(DISTINCT CAST(nc.DATA_EMISSAO AS DATE)) AS DIAS_ATIVOS
                    FROM NOTAS_CAB nc
                    {$fonte['join']}
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL AND nc.TOTAL_NOTA > 0
                      AND {$fonte['where']}
                      AND CAST({$fonte['col']} AS VARCHAR(50)) IN ({$inList})
                      {$fv}
                      {$fe}
                    GROUP BY {$fonte['col']}
                ";
                $rowsV = $this->pdo->query($sql)->fetchAll();
            } catch (\Throwable) { $rowsV = []; }

            // Devoluções (notas canceladas no período — qualquer fonte usa DATA_CANC)
            try {
                $sqlCanc = "
                    SELECT
                        {$fonte['col']} AS COD_V,
                        COUNT(*) AS QTD_CANC,
                        COALESCE(SUM(nc.TOTAL_NOTA), 0) AS VALOR_CANC
                    FROM NOTAS_CAB nc
                    {$fonte['join']}
                    WHERE nc.DATA_CANC BETWEEN '{$dataIni}' AND '{$dataFim}'
                      AND {$fonte['where']}
                      AND CAST({$fonte['col']} AS VARCHAR(50)) IN ({$inList})
                      {$fe}
                    GROUP BY {$fonte['col']}
                ";
                $rowsC = $this->pdo->query($sqlCanc)->fetchAll();
            } catch (\Throwable) { $rowsC = []; }

            $byCodV = [];
            foreach ($rowsV as $r) {
                $byCodV[(string)$r['COD_V']] = [
                    'clientes'    => (int)$r['CLIENTES'],
                    'primeira'    => $r['PRIMEIRA'],
                    'ultima'      => $r['ULTIMA'],
                    'dias_ativos' => (int)$r['DIAS_ATIVOS'],
                ];
            }
            $byCodC = [];
            foreach ($rowsC as $r) {
                $byCodC[(string)$r['COD_V']] = [
                    'qtd_canc'   => (int)$r['QTD_CANC'],
                    'valor_canc' => (float)$r['VALOR_CANC'],
                ];
            }

            $out = [];
            foreach ($topLinhas as $l) {
                $cod = (string)$l['COD_VENDEDOR'];
                $v = $byCodV[$cod] ?? ['clientes' => 0, 'primeira' => null, 'ultima' => null, 'dias_ativos' => 0];
                $c = $byCodC[$cod] ?? ['qtd_canc' => 0, 'valor_canc' => 0.0];
                $out[] = [
                    'cod'         => $cod,
                    'nome'        => $l['VENDEDOR'],
                    'total'       => (float)$l['TOTAL'],
                    'qtd_notas'   => (int)$l['QTD_NOTAS'],
                    'ticket'      => (float)$l['TICKET_MEDIO'],
                    'clientes'    => $v['clientes'],
                    'primeira'    => $v['primeira'],
                    'ultima'      => $v['ultima'],
                    'dias_ativos' => $v['dias_ativos'],
                    'qtd_canc'    => $c['qtd_canc'],
                    'valor_canc'  => $c['valor_canc'],
                ];
            }
            return $out;
        });
    }

    // -------------------------------------------------------------------------
    // Notas de saída (listagem — sem cache pois pode ter paginação/filtros variados)
    // -------------------------------------------------------------------------

    // =========================================================================
    // PRÉ-NOTAS / ORÇAMENTOS / PEDIDOS (v1.4.5+)
    // Tabela: PEDIDOS_CAB (com COMPRA_VENDA in 'V','O' e DT_EMISSAO no período)
    // Nem todas as lojas usam o módulo — quando vazio, view mostra estado amigável.
    // =========================================================================

    public function preNotas(string $dataIni, string $dataFim, int $limite = 1000): array
    {
        $limite = max(50, min(2000, $limite));
        $cacheKey = "pre_notas_{$dataIni}_{$dataFim}_{$limite}_" . ($this->codEmpresa ?? 'na');
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $limite) {
            $fe = $this->filtroEmpresa('p');
            try {
                $sql = "SELECT FIRST {$limite}
                            p.NR_PEDIDO,
                            p.DT_EMISSAO,
                            p.DT_PREVISAO,
                            p.HORA_EMISSAO,
                            p.COMPRA_VENDA,
                            p.COD_TIPO_PEDIDO,
                            TRIM(COALESCE(pt.DESCRICAO, '')) AS TIPO_DESC,
                            TRIM(COALESCE(p.NOME_CONSUMIDOR, COALESCE(pe.NOME, '(sem cliente)'))) AS CLIENTE,
                            TRIM(COALESCE(p.USUARIO_CRIOU, '')) AS USUARIO,
                            COALESCE(p.TOTAL_PEDIDO, 0) AS TOTAL_PEDIDO,
                            p.APROVADO,
                            p.DATA_APROVACAO,
                            TRIM(COALESCE(p.QUEM_APROVOU, '')) AS QUEM_APROVOU,
                            p.NRO_PED_VENDA,
                            p.NRO_ORCAMENTO
                        FROM PEDIDOS_CAB p
                        LEFT JOIN PESSOAS pe       ON pe.COD_PESSOA       = p.COD_PESSOA
                        LEFT JOIN PEDIDOS_TIPOS pt ON pt.COD_TIPO_PEDIDO  = p.COD_TIPO_PEDIDO
                        WHERE p.DT_EMISSAO BETWEEN '{$dataIni}' AND '{$dataFim}'
                          AND p.COMPRA_VENDA IN ('V','O')
                          {$fe}
                        ORDER BY p.DT_EMISSAO DESC, p.NR_PEDIDO DESC";
                return $this->utf8Nomes($this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC));
            } catch (\Throwable $e) {
                error_log('[preNotas] ' . $e->getMessage());
                return [];
            }
        });
    }

    /** Resumo agregado pra dashboard de pré-notas. */
    public function preNotasResumo(string $dataIni, string $dataFim): array
    {
        $cacheKey = "pre_notas_resumo_{$dataIni}_{$dataFim}_" . ($this->codEmpresa ?? 'na');
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            $fe = $this->filtroEmpresa('p');
            $base = "FROM PEDIDOS_CAB p
                     WHERE p.DT_EMISSAO BETWEEN '{$dataIni}' AND '{$dataFim}'
                       AND p.COMPRA_VENDA IN ('V','O')
                       {$fe}";

            // KPIs totais — quebra entre aprovados e pendentes
            $kpis = ['total' => 0, 'qtd' => 0, 'aprovados_qtd' => 0, 'aprovados_total' => 0.0,
                     'pendentes_qtd' => 0, 'pendentes_total' => 0.0, 'taxa_aprovacao' => null];
            try {
                $r = $this->pdo->query("
                    SELECT
                        COUNT(*) AS QTD,
                        COALESCE(SUM(p.TOTAL_PEDIDO), 0) AS TOTAL,
                        COUNT(CASE WHEN p.APROVADO = 'S' THEN 1 END) AS APROV_QTD,
                        COALESCE(SUM(CASE WHEN p.APROVADO = 'S' THEN p.TOTAL_PEDIDO ELSE 0 END), 0) AS APROV_TOTAL
                    {$base}
                ")->fetch(\PDO::FETCH_ASSOC);
                if ($r) {
                    $kpis['qtd']             = (int)$r['QTD'];
                    $kpis['total']           = (float)$r['TOTAL'];
                    $kpis['aprovados_qtd']   = (int)$r['APROV_QTD'];
                    $kpis['aprovados_total'] = (float)$r['APROV_TOTAL'];
                    $kpis['pendentes_qtd']   = $kpis['qtd']   - $kpis['aprovados_qtd'];
                    $kpis['pendentes_total'] = $kpis['total'] - $kpis['aprovados_total'];
                    $kpis['taxa_aprovacao']  = $kpis['qtd'] > 0 ? ($kpis['aprovados_qtd'] / $kpis['qtd'] * 100) : null;
                }
            } catch (\Throwable) {}

            // Por dia
            $porDia = [];
            try {
                $porDia = $this->pdo->query("
                    SELECT CAST(p.DT_EMISSAO AS DATE) AS DIA, COUNT(*) AS QTD,
                           COALESCE(SUM(p.TOTAL_PEDIDO), 0) AS TOTAL
                    {$base}
                    GROUP BY CAST(p.DT_EMISSAO AS DATE)
                    ORDER BY 1
                ")->fetchAll(\PDO::FETCH_ASSOC);
            } catch (\Throwable) {}

            // Por tipo de pedido
            $porTipo = [];
            try {
                $porTipo = $this->utf8Nomes($this->pdo->query("
                    SELECT TRIM(COALESCE(pt.DESCRICAO, COALESCE(p.COD_TIPO_PEDIDO, '(sem tipo)'))) AS TIPO,
                           COUNT(*) AS QTD,
                           COALESCE(SUM(p.TOTAL_PEDIDO), 0) AS TOTAL
                    FROM PEDIDOS_CAB p
                    LEFT JOIN PEDIDOS_TIPOS pt ON pt.COD_TIPO_PEDIDO = p.COD_TIPO_PEDIDO
                    WHERE p.DT_EMISSAO BETWEEN '{$dataIni}' AND '{$dataFim}'
                      AND p.COMPRA_VENDA IN ('V','O')
                      {$fe}
                    GROUP BY 1
                    ORDER BY 3 DESC
                ")->fetchAll(\PDO::FETCH_ASSOC));
            } catch (\Throwable) {}

            // Por usuário que criou (top 10 — só faz sentido se houver pedidos)
            $porUsuario = [];
            try {
                $porUsuario = $this->utf8Nomes($this->pdo->query("
                    SELECT FIRST 10 TRIM(COALESCE(p.USUARIO_CRIOU, '(sem usuário)')) AS USUARIO,
                           COUNT(*) AS QTD,
                           COALESCE(SUM(p.TOTAL_PEDIDO), 0) AS TOTAL
                    {$base}
                    GROUP BY TRIM(COALESCE(p.USUARIO_CRIOU, '(sem usuário)'))
                    ORDER BY 3 DESC
                ")->fetchAll(\PDO::FETCH_ASSOC));
            } catch (\Throwable) {}

            // Top 10 maiores
            $topMaiores = [];
            try {
                $topMaiores = $this->utf8Nomes($this->pdo->query("
                    SELECT FIRST 10 p.NR_PEDIDO, p.DT_EMISSAO, p.TOTAL_PEDIDO, p.APROVADO,
                           TRIM(COALESCE(p.NOME_CONSUMIDOR, COALESCE(pe.NOME, '(sem cliente)'))) AS CLIENTE
                    FROM PEDIDOS_CAB p
                    LEFT JOIN PESSOAS pe ON pe.COD_PESSOA = p.COD_PESSOA
                    WHERE p.DT_EMISSAO BETWEEN '{$dataIni}' AND '{$dataFim}'
                      AND p.COMPRA_VENDA IN ('V','O')
                      {$fe}
                    ORDER BY p.TOTAL_PEDIDO DESC
                ")->fetchAll(\PDO::FETCH_ASSOC));
            } catch (\Throwable) {}

            return [
                'kpis'        => $kpis,
                'por_dia'     => $porDia,
                'por_tipo'    => $porTipo,
                'por_usuario' => $porUsuario,
                'top_maiores' => $topMaiores,
            ];
        });
    }

    // =========================================================================
    // NOTAS DE SAÍDA v2 (v1.4.4+) — endpoint enriquecido
    //   Diferencas vs notasSaida() original:
    //     • Usa filtroVenda() (portável entre lojas) em vez de IDs hardcoded
    //     • Vendedor via mesma heurística do vendedorAprofundado (fonteVendedor)
    //     • Mais colunas: COD_TIPO_OPERACAO, OPERACAO, CLIENTE, STATUS, CANCELADA
    //   NÃO substitui o método antigo — coexistem.
    // =========================================================================
    public function notasSaidaV2(string $dataIni, string $dataFim, int $limite = 1000): array
    {
        $limite = max(50, min(2000, $limite));
        $cacheKey = "notas_saida_v2_{$dataIni}_{$dataFim}_{$limite}_" . ($this->codEmpresa ?? 'na');
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $limite) {
            $iv = $this->intervaloData($dataIni, $dataFim, 'nc');
            $fe = $this->filtroEmpresa('nc');
            $fv = $this->filtroVenda('nc');
            $fonte = $this->fonteVendedor($dataIni, $dataFim);

            // Monta SELECT do vendedor conforme fonte detectada
            $selVend = "''";
            $joinVend = '';
            if ($fonte) {
                $selVend  = $fonte['nome'];
                $joinVend = $fonte['join'];
            }

            try {
                $sql = "SELECT FIRST {$limite}
                            nc.NR_NOTA, nc.SERIE, nc.DATA_EMISSAO, nc.DATA_HORA_VENDA,
                            nc.COD_SITUACAO, nc.TOTAL_NOTA, nc.DATA_CANC,
                            nc.COD_TIPO_OPERACAO,
                            TRIM(COALESCE(op.DESCRICAO, '')) AS OPERACAO,
                            TRIM(COALESCE(p.NOME, '')) AS CLIENTE,
                            TRIM(COALESCE({$selVend}, '')) AS VENDEDOR
                        FROM NOTAS_CAB nc
                        LEFT JOIN PESSOAS p ON p.COD_PESSOA = nc.COD_PESSOA
                        LEFT JOIN TIPO_OPERACAO op
                               ON op.COD_TIPO_OPERACAO = nc.COD_TIPO_OPERACAO
                              AND op.COD_EMPRESA       = nc.COD_EMPRESA
                        {$joinVend}
                        WHERE {$iv}
                          {$fv}
                          {$fe}
                        ORDER BY nc.DATA_EMISSAO DESC, nc.NR_NOTA DESC";
                return $this->utf8Nomes($this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC));
            } catch (\Throwable $e) {
                error_log('[notasSaidaV2] ' . $e->getMessage());
                return [];
            }
        });
    }

    /**
     * Resumo agregado pra tela Notas de Saída.
     * Retorna: por_serie, por_dia, top_maiores, por_hora_dsem, canceladas
     */
    public function notasSaidaResumo(string $dataIni, string $dataFim): array
    {
        $cacheKey = "notas_saida_resumo_{$dataIni}_{$dataFim}_" . ($this->codEmpresa ?? 'na');
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            $iv = $this->intervaloData($dataIni, $dataFim, 'nc');
            $fe = $this->filtroEmpresa('nc');
            $fv = $this->filtroVenda('nc');

            $base = "FROM NOTAS_CAB nc
                     WHERE {$iv}
                       AND nc.DATA_CANC IS NULL AND nc.TOTAL_NOTA > 0
                       {$fv}
                       {$fe}";

            // Por série
            $porSerie = [];
            try {
                $porSerie = $this->pdo->query("
                    SELECT TRIM(nc.SERIE) AS SERIE, COUNT(*) AS QTD,
                           COALESCE(SUM(nc.TOTAL_NOTA), 0) AS TOTAL
                    {$base}
                    GROUP BY TRIM(nc.SERIE)
                    ORDER BY 3 DESC
                ")->fetchAll(\PDO::FETCH_ASSOC);
            } catch (\Throwable) { $porSerie = []; }

            // Por dia
            $porDia = [];
            try {
                $porDia = $this->pdo->query("
                    SELECT CAST(nc.DATA_EMISSAO AS DATE) AS DIA, COUNT(*) AS QTD,
                           COALESCE(SUM(nc.TOTAL_NOTA), 0) AS TOTAL,
                           COALESCE(AVG(nc.TOTAL_NOTA), 0) AS TICKET
                    {$base}
                    GROUP BY CAST(nc.DATA_EMISSAO AS DATE)
                    ORDER BY 1
                ")->fetchAll(\PDO::FETCH_ASSOC);
            } catch (\Throwable) { $porDia = []; }

            // Top 10 maiores notas do período
            $topMaiores = [];
            try {
                $topMaiores = $this->utf8Nomes($this->pdo->query("
                    SELECT FIRST 10 nc.NR_NOTA, TRIM(nc.SERIE) AS SERIE,
                           nc.DATA_EMISSAO, nc.TOTAL_NOTA,
                           TRIM(COALESCE(p.NOME, '(consumidor)')) AS CLIENTE
                    FROM NOTAS_CAB nc
                    LEFT JOIN PESSOAS p ON p.COD_PESSOA = nc.COD_PESSOA
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL AND nc.TOTAL_NOTA > 0
                      {$fv}
                      {$fe}
                    ORDER BY nc.TOTAL_NOTA DESC, nc.DATA_EMISSAO DESC
                ")->fetchAll(\PDO::FETCH_ASSOC));
            } catch (\Throwable) { $topMaiores = []; }

            // Por hora × dia-da-semana (heatmap)
            $porHoraDsem = [];
            try {
                $rows = $this->pdo->query("
                    SELECT
                        EXTRACT(WEEKDAY FROM nc.DATA_EMISSAO) AS DSEM,
                        EXTRACT(HOUR FROM nc.DATA_HORA_VENDA) AS HORA,
                        COUNT(*) AS QTD
                    {$base}
                      AND nc.DATA_HORA_VENDA IS NOT NULL
                    GROUP BY 1, 2
                    ORDER BY 1, 2
                ")->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as $r) {
                    $porHoraDsem[] = [(int)$r['DSEM'], (int)$r['HORA'], (int)$r['QTD']];
                }
            } catch (\Throwable) { $porHoraDsem = []; }

            // Canceladas no período
            $canceladas = ['qtd' => 0, 'valor' => 0.0];
            try {
                $r = $this->pdo->query("
                    SELECT COUNT(*) AS QTD, COALESCE(SUM(nc.TOTAL_NOTA), 0) AS VALOR
                    FROM NOTAS_CAB nc
                    WHERE nc.DATA_CANC BETWEEN '{$dataIni}' AND '{$dataFim}'
                      {$fv}
                      {$fe}
                ")->fetch(\PDO::FETCH_ASSOC);
                if ($r) $canceladas = ['qtd' => (int)$r['QTD'], 'valor' => (float)$r['VALOR']];
            } catch (\Throwable) {}

            return [
                'por_serie'      => $porSerie,
                'por_dia'        => $porDia,
                'top_maiores'    => $topMaiores,
                'por_hora_dsem'  => $porHoraDsem,
                'canceladas'     => $canceladas,
            ];
        });
    }

    public function notasSaida(string $dataIni, string $dataFim, ?int $codVendedor = null, int $limite = 200): array
    {
        $cacheKey   = "notas_saida_{$dataIni}_{$dataFim}_" . ($codVendedor ?? '0') . "_{$limite}";
        $filtroVend = $codVendedor ? "AND nc.COD_PESSOA_VENDA = {$codVendedor}" : '';

        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $filtroVend, $limite) {
            try {
                $sql = "SELECT FIRST {$limite}
                        nc.NR_NOTA, nc.SERIE, nc.DATA_EMISSAO, nc.DATA_HORA_VENDA,
                        nc.COD_SITUACAO, nc.TOTAL_NOTA,
                        TRIM(COALESCE(p.NOME, '')) AS CLIENTE,
                        TRIM(COALESCE(pv.NOME, '')) AS VENDEDOR
                    FROM NOTAS_CAB nc
                    LEFT JOIN PESSOAS p  ON p.COD_PESSOA  = nc.COD_PESSOA
                    LEFT JOIN PESSOAS pv ON pv.COD_PESSOA = nc.COD_PESSOA_VENDA
                    WHERE {$this->intervaloData($dataIni, $dataFim, 'nc')}
                      AND nc.COD_TIPO_OPERACAO IN (115,231,297)
                      AND nc.COD_SITUACAO <> 3
                      {$filtroVend}
                    ORDER BY nc.DATA_EMISSAO DESC, nc.NR_NOTA DESC";
                return $this->utf8Nomes($this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC));
            } catch (\Throwable $e) {
                error_log('[notasSaida] ' . $e->getMessage());
                return [];
            }
        });
    }

    // -------------------------------------------------------------------------
    // CUSTO MÉDIO POR VENDA (cupom) — espelho do Custo Médio de Compra.
    // Por cupom: custo médio por item = Σ(custo dos itens) ÷ total de peças.
    // Mostra também o ticket por item (venda ÷ peças) e a margem da venda.
    // Custo do item vendido: 1º a linha da venda (CUSTO_FINAL/ULT), 2º o CADASTRO DE
    // ESTOQUE (ITENS_ESTOQUE) — no Achadinhos ~24% das linhas de venda vêm sem custo,
    // mas o estoque tem CUSTO_COMPRA preenchido em ~95%. Exige LEFT JOIN ITENS_ESTOQUE ie.
    // -------------------------------------------------------------------------
    private const CUSTO_ITEM = "COALESCE(NULLIF(ni.CUSTO_FINAL,0),NULLIF(ni.CUSTO_ULT_COMPRA,0),NULLIF(ie.CUSTO_FINAL,0),NULLIF(ie.CUSTO_COMPRA,0),NULLIF(ie.CUSTO_MEDIO,0),0)";

    public function custoMedioVenda(array $f): array
    {
        $D = fn($v) => (is_string($v) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $v)) ? $v : null;
        $ini = $D($f['data_ini'] ?? null) ?: date('Y-m-d');
        $fim = $D($f['data_fim'] ?? null) ?: $ini;
        if ($fim < $ini) $fim = $ini;
        $busca   = trim((string)($f['busca'] ?? ''));
        $ordenar = (string)($f['ordenar'] ?? 'custo'); // custo|margem|valor|data|itens
        $apenas  = (string)($f['apenas'] ?? '');        // '' | sem_custo | margem_baixa
        $lim     = max(20, min(5000, (int)($f['limite'] ?? 1000)));

        $iv = $this->intervaloData($ini, $fim, 'nc');
        $fv = $this->filtroVenda('nc');
        $fe = $this->filtroEmpresa('nc');   // também carrega self::$codEmpresa
        $cu = self::CUSTO_ITEM;

        $base = Cache::lembrar(
            'vendas_customedio_' . md5(json_encode([$ini, $fim, self::$codEmpresa, $lim])),
            $this->ttl($fim),
            function () use ($iv, $fv, $fe, $cu, $lim) {
                $utf = fn($s) => ($s !== null && $s !== '' && !mb_check_encoding((string)$s, 'UTF-8'))
                    ? mb_convert_encoding((string)$s, 'UTF-8', 'Windows-1252') : (string)$s;

                $rows = $this->pdo->query("
                    SELECT FIRST {$lim} nc.NR_NOTA, nc.COD_TIPO_OPERACAO OP, nc.DATA_EMISSAO DT,
                           TRIM(COALESCE(p.NOME,'Consumidor')) CLIENTE,
                           COALESCE(SUM(ni.QTD),0) PECAS,
                           COALESCE(SUM(CAST(ni.QTD AS DOUBLE PRECISION)*ni.PRECO_UNIT),0) VENDA,
                           COALESCE(SUM(CAST(ni.QTD AS DOUBLE PRECISION)*{$cu}),0) CUSTO
                    FROM NOTAS_CAB nc
                    JOIN NOTAS_ITENS ni ON ni.NR_NOTA = nc.NR_NOTA
                                       AND ni.COD_TIPO_OPERACAO = nc.COD_TIPO_OPERACAO
                                       AND ni.COD_EMPRESA = nc.COD_EMPRESA
                    LEFT JOIN ITENS_ESTOQUE ie ON ie.COD_ITEM = ni.COD_ITEM AND ie.COD_EMPRESA = ni.COD_EMPRESA
                    LEFT JOIN PESSOAS p ON p.COD_PESSOA = nc.COD_PESSOA
                    WHERE {$iv} AND nc.DATA_CANC IS NULL {$fv} {$fe}
                    GROUP BY nc.NR_NOTA, nc.COD_TIPO_OPERACAO, nc.DATA_EMISSAO, TRIM(COALESCE(p.NOME,'Consumidor'))
                    ORDER BY nc.NR_NOTA DESC
                ")->fetchAll(\PDO::FETCH_ASSOC);

                $cupons = [];
                foreach ($rows as $r) {
                    $pecas = (float)$r['PECAS'];
                    if ($pecas <= 0) continue;
                    $venda = (float)$r['VENDA']; $custo = (float)$r['CUSTO'];
                    $dt = is_object($r['DT']) ? $r['DT']->format('Y-m-d') : substr((string)$r['DT'], 0, 10);
                    $cupons[] = [
                        'nr_nota'     => (int)$r['NR_NOTA'], 'op' => (int)$r['OP'], 'data' => $dt,
                        'cliente'     => $utf($r['CLIENTE']),
                        'pecas'       => $pecas, 'venda' => $venda, 'custo' => $custo,
                        'custo_medio' => $pecas > 0 ? $custo / $pecas : 0.0,
                        'ticket_item' => $pecas > 0 ? $venda / $pecas : 0.0,
                        'margem'      => $venda > 0 ? ($venda - $custo) / $venda * 100 : 0.0,
                        'sem_custo'   => $custo <= 0,
                    ];
                }

                // KPIs sobre o período inteiro (não só o que foi capado)
                $k = $this->pdo->query("
                    SELECT COUNT(DISTINCT nc.NR_NOTA) CUPONS, COALESCE(SUM(ni.QTD),0) PECAS,
                           COALESCE(SUM(CAST(ni.QTD AS DOUBLE PRECISION)*ni.PRECO_UNIT),0) VENDA, COALESCE(SUM(CAST(ni.QTD AS DOUBLE PRECISION)*{$cu}),0) CUSTO
                    FROM NOTAS_CAB nc
                    JOIN NOTAS_ITENS ni ON ni.NR_NOTA = nc.NR_NOTA
                                       AND ni.COD_TIPO_OPERACAO = nc.COD_TIPO_OPERACAO
                                       AND ni.COD_EMPRESA = nc.COD_EMPRESA
                    LEFT JOIN ITENS_ESTOQUE ie ON ie.COD_ITEM = ni.COD_ITEM AND ie.COD_EMPRESA = ni.COD_EMPRESA
                    WHERE {$iv} AND nc.DATA_CANC IS NULL {$fv} {$fe}
                ")->fetch(\PDO::FETCH_ASSOC) ?: [];

                return ['cupons' => $cupons, 'kpis' => $k, 'capado' => count($rows) >= $lim];
            }
        );

        // Pós-processamento em PHP (não re-consulta o banco).
        $cupons = $base['cupons'];
        if ($busca !== '') {
            $bu = mb_strtoupper($busca, 'UTF-8');
            $cupons = array_values(array_filter($cupons, fn($c) =>
                mb_stripos((string)$c['cliente'], $bu) !== false || stripos((string)$c['nr_nota'], $bu) !== false));
        }
        if ($apenas === 'sem_custo')        $cupons = array_values(array_filter($cupons, fn($c) => $c['sem_custo']));
        elseif ($apenas === 'margem_baixa') $cupons = array_values(array_filter($cupons, fn($c) => !$c['sem_custo'] && $c['margem'] < 20));

        usort($cupons, function ($a, $b) use ($ordenar) {
            if ($ordenar === 'margem') return $a['margem'] <=> $b['margem'];      // pior margem primeiro
            if ($ordenar === 'valor')  return $b['venda'] <=> $a['venda'];
            if ($ordenar === 'itens')  return $b['pecas'] <=> $a['pecas'];
            if ($ordenar === 'data')   return strcmp((string)$b['data'], (string)$a['data']);
            return $b['custo_medio'] <=> $a['custo_medio'];                       // custo médio/item desc
        });

        $k  = $base['kpis'];
        $kp = (float)($k['PECAS'] ?? 0); $kv = (float)($k['VENDA'] ?? 0); $kc = (float)($k['CUSTO'] ?? 0);
        $resumo = [
            'CUPONS'      => (int)($k['CUPONS'] ?? 0),
            'PECAS'       => $kp, 'VENDA' => $kv, 'CUSTO' => $kc,
            'CUSTO_MEDIO' => $kp > 0 ? $kc / $kp : 0.0,
            'TICKET_ITEM' => $kp > 0 ? $kv / $kp : 0.0,
            'MARGEM'      => $kv > 0 ? ($kv - $kc) / $kv * 100 : 0.0,
        ];

        return [
            'cupons'  => array_slice($cupons, 0, $lim),
            'resumo'  => $resumo,
            'dataIni' => $ini, 'dataFim' => $fim,
            'capado'  => $base['capado'],
            'ordenar' => $ordenar, 'apenas' => $apenas, 'busca' => $busca,
        ];
    }

    /** Itens de um cupom (drill sob demanda da tela Custo Médio por Venda). */
    public function itensCupom(int $nr, int $op): array
    {
        $cu = self::CUSTO_ITEM;
        $fe = $this->filtroEmpresa('ni');
        $utf = fn($s) => ($s !== null && $s !== '' && !mb_check_encoding((string)$s, 'UTF-8'))
            ? mb_convert_encoding((string)$s, 'UTF-8', 'Windows-1252') : (string)$s;
        $rows = $this->pdo->query("
            SELECT ni.COD_ITEM, TRIM(MAX(i.DESCRICAO)) DESCRICAO,
                   COALESCE(SUM(ni.QTD),0) QTD,
                   COALESCE(SUM(CAST(ni.QTD AS DOUBLE PRECISION)*ni.PRECO_UNIT),0) VENDA,
                   COALESCE(SUM(CAST(ni.QTD AS DOUBLE PRECISION)*{$cu}),0) CUSTO
            FROM NOTAS_ITENS ni
            LEFT JOIN ITENS i ON i.COD_ITEM = ni.COD_ITEM
            LEFT JOIN ITENS_ESTOQUE ie ON ie.COD_ITEM = ni.COD_ITEM AND ie.COD_EMPRESA = ni.COD_EMPRESA
            WHERE ni.NR_NOTA = {$nr} AND ni.COD_TIPO_OPERACAO = {$op} {$fe}
            GROUP BY ni.COD_ITEM
            ORDER BY 5 DESC
        ")->fetchAll(\PDO::FETCH_ASSOC);
        $out = [];
        foreach ($rows as $r) {
            $qtd = (float)$r['QTD']; $venda = (float)$r['VENDA']; $custo = (float)$r['CUSTO'];
            $out[] = [
                'cod'      => (int)$r['COD_ITEM'], 'desc' => $utf($r['DESCRICAO']),
                'qtd'      => $qtd, 'venda' => $venda, 'custo' => $custo,
                'venda_un' => $qtd > 0 ? $venda / $qtd : 0.0,
                'custo_un' => $qtd > 0 ? $custo / $qtd : 0.0,
                'margem'   => $venda > 0 ? ($venda - $custo) / $venda * 100 : 0.0,
            ];
        }
        return $out;
    }

    // -------------------------------------------------------------------------
    // Intervalo padrão (último dia com dados)
    // -------------------------------------------------------------------------

    public function ultimaData(): string
    {
        return $this->ultimaDataComVendas();
    }

    public function ultimaDataComVendas(): string
    {
        return Cache::lembrar('vendas_ultima_data', 600, function () {
            // MAX só na JANELA RECENTE (120d): usa índice ascendente do range. Buscar o MAX/
            // ORDER BY DESC sem limite custa ~45s no Firebird 2.5 (sem índice descendente).
            $jan = "DATA_EMISSAO >= DATEADD(DAY,-120,CURRENT_DATE) AND DATA_EMISSAO <= CURRENT_DATE";
            $tentativas = [
                "SELECT MAX(DATA_EMISSAO) FROM NOTAS_CAB WHERE DATA_CANC IS NULL AND TOTAL_NOTA > 0 AND {$jan}",
                "SELECT MAX(DATA_EMISSAO) FROM NOTAS_CAB WHERE COD_TIPO_OPERACAO IN (115,231,297) AND COD_SITUACAO <> 3 AND {$jan}",
                "SELECT MAX(DATA_EMISSAO) FROM NOTAS_CAB WHERE DATA_CANC IS NULL AND {$jan}",
                "SELECT MAX(DATA_EMISSAO) FROM NOTAS_CAB WHERE {$jan}",
            ];
            foreach ($tentativas as $sql) {
                try {
                    $v = $this->escalar($sql);
                    if ($v) {
                        $d = $v instanceof \DateTime ? $v->format('Y-m-d') : substr(trim((string)$v), 0, 10);
                        if ($d > '2000-01-01') return $d;
                    }
                } catch (\Throwable) {}
            }
            return date('Y-m-d');
        });
    }

    public function anosDisponiveis(): array
    {
        return Cache::lembrar('vendas_anos_disponiveis', 3600, function () {
            // FIRST 1 ASC/DESC com COD_EMPRESA usa índice (COD_EMPRESA, DATA_EMISSAO)
            // — sem full scan DISTINCT em 1.3M linhas
            $this->filtroEmpresa(''); // garante que self::$codEmpresa esteja carregado
            $feCond = self::$codEmpresa ? "AND COD_EMPRESA = " . self::$codEmpresa : '';
            try {
                // Ano teto = ano ATUAL (não busca o MAX da data: 1) venda não é no futuro;
                // 2) buscar MAX/ORDER BY DESC sem índice descendente custa ~45s no Firebird 2.5).
                // Ano início = MIN da data (usa índice ascendente — instantâneo). EXTRACT devolve
                // o ano como número (não depende do formato de texto da data).
                $anoAtual = (int) date('Y');
                $anoIni   = (int) $this->escalar("SELECT EXTRACT(YEAR FROM MIN(DATA_EMISSAO)) FROM NOTAS_CAB WHERE DATA_CANC IS NULL {$feCond}");
                if ($anoIni < 2010 || $anoIni > $anoAtual) $anoIni = $anoAtual;
                $anoFim = $anoAtual;
                if (($anoFim - $anoIni) > 20) $anoIni = $anoFim - 20; // teto de segurança
                return array_reverse(range($anoIni, $anoFim));
            } catch (\Throwable) { return [(int)date('Y')]; }
        });
    }

    // -------------------------------------------------------------------------
    // Faturamento diário por série (pivot para tabela)
    // -------------------------------------------------------------------------

    public function faturamentoDiarioPorSerie(string $dataIni, string $dataFim): array
    {
        $cacheKey = "fat_diario_serie_{$dataIni}_{$dataFim}";
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            try {
                $rows = $this->pdo->query("
                    SELECT DATA_EMISSAO, TRIM(SERIE) AS SERIE,
                           COALESCE(SUM(TOTAL_NOTA), 0) AS TOTAL
                    FROM NOTAS_CAB
                    WHERE {$this->intervaloData($dataIni, $dataFim, '')}
                      AND DATA_CANC IS NULL AND TOTAL_NOTA > 0
                      {$this->filtroVenda('')}
                      {$this->filtroEmpresa('')}
                    GROUP BY DATA_EMISSAO, TRIM(SERIE)
                    ORDER BY DATA_EMISSAO, TRIM(SERIE)
                ")->fetchAll();
            } catch (\Throwable) { return []; }

            $pivot = []; $series = [];
            foreach ($rows as $r) {
                $data  = is_object($r['DATA_EMISSAO'])
                       ? $r['DATA_EMISSAO']->format('Y-m-d')
                       : substr(trim((string)$r['DATA_EMISSAO']), 0, 10);
                $serie = (string)$r['SERIE'];
                $pivot[$data][$serie] = ($pivot[$data][$serie] ?? 0.0) + (float)$r['TOTAL'];
                $series[$serie] = true;
            }
            ksort($pivot); ksort($series);

            $totaisDia = [];
            foreach ($pivot as $data => $sd) $totaisDia[$data] = array_sum($sd);

            return ['pivot' => $pivot, 'series' => array_keys($series), 'totais_dia' => $totaisDia];
        });
    }

    // -------------------------------------------------------------------------
    // Faturamento diário por modelo fiscal (pivot)
    // -------------------------------------------------------------------------

    public function faturamentoDiarioPorModelo(string $dataIni, string $dataFim): array
    {
        $cacheKey = "fat_diario_modelo_{$dataIni}_{$dataFim}";
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            try {
                $rows = $this->pdo->query("
                    SELECT DATA_EMISSAO, TRIM(SERIE) AS SERIE,
                           COALESCE(SUM(TOTAL_NOTA), 0) AS TOTAL
                    FROM NOTAS_CAB
                    WHERE {$this->intervaloData($dataIni, $dataFim, '')}
                      AND DATA_CANC IS NULL AND TOTAL_NOTA > 0
                      {$this->filtroVenda('')}
                      {$this->filtroEmpresa('')}
                    GROUP BY DATA_EMISSAO, TRIM(SERIE)
                    ORDER BY DATA_EMISSAO
                ")->fetchAll();
            } catch (\Throwable) { return []; }

            $pivot = []; $modelos = [];
            foreach ($rows as $r) {
                $data  = is_object($r['DATA_EMISSAO'])
                       ? $r['DATA_EMISSAO']->format('Y-m-d')
                       : substr(trim((string)$r['DATA_EMISSAO']), 0, 10);
                $serie = strtoupper(trim((string)$r['SERIE']));
                $modelo = str_starts_with($serie, 'NF') ? '65'
                        : (is_numeric($serie) ? '55' : 'SN');

                $pivot[$data][$modelo] = ($pivot[$data][$modelo] ?? 0.0) + (float)$r['TOTAL'];
                $modelos[$modelo] = true;
            }
            ksort($pivot);

            $totaisDia = [];
            foreach ($pivot as $data => $md) $totaisDia[$data] = array_sum($md);

            $ordem = ['55', '65', 'SN'];
            $modelosOrd = array_values(array_filter($ordem, fn($m) => isset($modelos[$m])));

            return ['pivot' => $pivot, 'modelos' => $modelosOrd, 'totais_dia' => $totaisDia];
        });
    }

    // -------------------------------------------------------------------------
    // Vendas por dia da semana — com custo, markup, lucro, ticket
    // -------------------------------------------------------------------------

    public function vendasDiaSemanaExtendido(string $dataIni, string $dataFim): array
    {
        $cacheKey = "vendas_diasemana_ext_{$dataIni}_{$dataFim}";
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            $rows = [];
            // Tenta várias colunas de custo no NOTAS_ITENS (com filtro de data para evitar full scan)
            foreach (['CUSTO_UNIT', 'PRECO_CUSTO', 'CUSTO_MEDIO', 'VLR_CUSTO'] as $col) {
                try {
                    $rows = $this->pdo->query("
                        SELECT
                            EXTRACT(WEEKDAY FROM nc.DATA_EMISSAO)     AS DIA_NUM,
                            COUNT(*)                                   AS QTD_NOTAS,
                            COALESCE(SUM(nc.TOTAL_NOTA), 0)           AS VENDA,
                            COALESCE(SUM(c.CUSTO_TOTAL), 0)           AS CUSTO
                        FROM NOTAS_CAB nc
                        LEFT JOIN (
                            SELECT NR_NOTA, SERIE, COD_EMPRESA, COD_TIPO_OPERACAO,
                                   SUM({$col} * QTD) AS CUSTO_TOTAL
                            FROM NOTAS_ITENS
                            WHERE {$this->intervaloData($dataIni, $dataFim, '')}
                            GROUP BY NR_NOTA, SERIE, COD_EMPRESA, COD_TIPO_OPERACAO
                        ) c ON c.NR_NOTA           = nc.NR_NOTA
                           AND c.SERIE             = nc.SERIE
                           AND c.COD_EMPRESA       = nc.COD_EMPRESA
                           AND c.COD_TIPO_OPERACAO = nc.COD_TIPO_OPERACAO
                        WHERE {$this->intervaloData($dataIni, $dataFim, 'nc')}
                          AND nc.DATA_CANC IS NULL AND nc.TOTAL_NOTA > 0
                          {$this->filtroVenda('nc')}
                          {$this->filtroEmpresa('nc')}
                        GROUP BY EXTRACT(WEEKDAY FROM nc.DATA_EMISSAO)
                        ORDER BY DIA_NUM
                    ")->fetchAll();
                    if (!empty($rows)) break;
                } catch (\Throwable) {}
            }

            // Fallback sem custo
            if (empty($rows)) {
                try {
                    $rows = $this->pdo->query("
                        SELECT EXTRACT(WEEKDAY FROM DATA_EMISSAO) AS DIA_NUM,
                               COUNT(*) AS QTD_NOTAS,
                               COALESCE(SUM(TOTAL_NOTA), 0) AS VENDA, 0 AS CUSTO
                        FROM NOTAS_CAB
                        WHERE {$this->intervaloData($dataIni, $dataFim, '')}
                          AND DATA_CANC IS NULL AND TOTAL_NOTA > 0
                          {$this->filtroVenda('')}
                          {$this->filtroEmpresa('')}
                        GROUP BY EXTRACT(WEEKDAY FROM DATA_EMISSAO)
                        ORDER BY DIA_NUM
                    ")->fetchAll();
                } catch (\Throwable) { return []; }
            }

            // Ordem seg→dom (Firebird: 0=Dom,1=Seg,...,6=Sáb)
            $ordem  = [1, 2, 3, 4, 5, 6, 0];
            $nomes  = [0=>'Domingo', 1=>'Segunda-feira', 2=>'Terça-feira',
                       3=>'Quarta-feira', 4=>'Quinta-feira', 5=>'Sexta-feira', 6=>'Sábado'];
            $mapa   = array_fill(0, 7, ['qtd'=>0,'venda'=>0.0,'custo'=>0.0,'dia'=>'']);
            foreach ($nomes as $i => $n) $mapa[$i]['dia'] = $n;

            $totalVenda = 0.0;
            foreach ($rows as $r) {
                $idx = (int)$r['DIA_NUM'];
                if ($idx >= 0 && $idx <= 6) {
                    $mapa[$idx]['qtd']   = (int)$r['QTD_NOTAS'];
                    $mapa[$idx]['venda'] = round((float)$r['VENDA'], 2);
                    $mapa[$idx]['custo'] = round((float)$r['CUSTO'], 2);
                    $totalVenda += $mapa[$idx]['venda'];
                }
            }

            foreach ($mapa as &$item) {
                $v = $item['venda']; $c = $item['custo'];
                $item['lucro_rs']  = round($v - $c, 2);
                $item['lucro_pct'] = $v > 0  ? round(($v - $c) / $v * 100, 2) : 0.0;
                $item['markup']    = $c > 0  ? round(($v - $c) / $c * 100, 2) : 0.0;
                $item['ticket']    = $item['qtd'] > 0 ? round($v / $item['qtd'], 2) : 0.0;
                $item['pct_total'] = $totalVenda > 0 ? round($v / $totalVenda * 100, 2) : 0.0;
            }
            unset($item);

            // Reordena seg→dom
            return array_map(fn($i) => $mapa[$i], $ordem);
        });
    }

    // -------------------------------------------------------------------------
    // Utilitário
    // -------------------------------------------------------------------------

    private function escalar(string $sql): mixed
    {
        try {
            return $this->pdo->query($sql)->fetchColumn();
        } catch (\PDOException) {
            return 0;
        }
    }

    // =========================================================================
    // VENDAS POR GRUPO / MARCA  (módulo Vendas — paridade com Por Vendedor)
    // Depto de Engenharia e Automação - Master Automação - Autor: Derlivon Silva
    // =========================================================================
    //
    // Schema observado (Fire/ERP do Real Prime):
    //   - Itens em "ITENS" (NÃO "PRODUTOS"); JOIN ITENS i ON i.COD_ITEM = ni.COD_ITEM
    //     (ITENS não tem COD_EMPRESA — não juntar por ele).
    //   - "Grupo" conceitual = tabela CLASSE (COD_CLASSE, DESCRICAO);
    //     ITENS.COD_CLASSE pode estar NULL em muitos itens.
    //   - "Marca" = tabela MARCA (COD_MARCA, DESCRICAO); ITENS.COD_MARCA também
    //     pode estar predominantemente NULL.
    //   - NOTAS_ITENS tem COD_MARCA redundante; usamos sempre a do ITENS p/ consistência.
    //   - NOTAS_ITENS junta a NOTAS_CAB por NR_NOTA + SERIE + COD_TIPO_OPERACAO + COD_EMPRESA.
    //   - Colunas de qtd/valor: QTD e PRECO_UNIT (não QTDE/VALOR_UNIT).
    //
    // Quando 100% dos itens vendidos estão sem grupo/marca preenchido, retorna
    // 1 bucket "(sem grupo)" / "(sem marca)" com todo o faturamento — a tela
    // mostra isso pro usuário de boa fé (decisão do cadastro do ERP, não do BI).
    // -------------------------------------------------------------------------

    /**
     * Retorna ranking de vendas por GRUPO (ESTRUTURA) no período.
     *
     * Estrutura do retorno (paridade com vendasPorMarca):
     *   [
     *     'linhas'     => [...Top N detalhado...],          // mantém compat com chamadas antigas
     *     'outros'     => {nome,faturamento,qtd_notas,...} | null,
     *     'total_real' => {faturamento, qtd_notas, qtd_grupos, qtd_itens},
     *   ]
     *
     * IMPORTANTE: o retorno mudou — virou array associativo com chave 'linhas'.
     * Chamadas antigas que tratavam o retorno como lista pura devem usar $r['linhas'].
     */
    public function vendasPorGrupo(string $dataIni, string $dataFim, int $limite = 50): array
    {
        $limite = max(1, min(500, $limite));
        $cacheKey = "vendas_por_grupo_v2_{$dataIni}_{$dataFim}_{$limite}_" . (self::$codEmpresa ?? 'na');
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $limite) {
            $iv = $this->intervaloData($dataIni, $dataFim, 'nc');
            $fe = $this->filtroEmpresa('nc');
            $fv = $this->filtroVenda('nc');

            // (a) Top N detalhado
            $rows = [];
            try {
                $sql = "
                    SELECT FIRST {$limite}
                        i.COD_ESTRUTURA                                             AS COD_GRUPO,
                        TRIM(COALESCE(e.DESCRICAO, '(sem grupo)'))                  AS DESCRICAO,
                        COUNT(DISTINCT nc.NR_NOTA || '|' || nc.SERIE || '|' ||
                                       nc.COD_TIPO_OPERACAO || '|' || nc.COD_EMPRESA) AS QTD_NOTAS,
                        COALESCE(SUM(ni.QTD), 0)                                    AS QTD_ITENS,
                        COALESCE(SUM(CAST(ni.QTD AS DOUBLE PRECISION) * ni.PRECO_UNIT), 0) AS FATURAMENTO
                    FROM NOTAS_CAB nc
                    JOIN NOTAS_ITENS ni
                         ON ni.NR_NOTA          = nc.NR_NOTA
                        AND ni.SERIE            = nc.SERIE
                        AND ni.COD_TIPO_OPERACAO = nc.COD_TIPO_OPERACAO
                        AND ni.COD_EMPRESA      = nc.COD_EMPRESA
                    LEFT JOIN ITENS     i ON i.COD_ITEM        = ni.COD_ITEM
                    LEFT JOIN ESTRUTURA e ON e.COD_ESTRUTURA   = i.COD_ESTRUTURA
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      {$fv}
                      {$fe}
                    GROUP BY i.COD_ESTRUTURA, TRIM(COALESCE(e.DESCRICAO, '(sem grupo)'))
                    ORDER BY FATURAMENTO DESC
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
            } catch (\Throwable) {
                $rows = [];
            }

            // (b) Agregado total REAL do universo (somatório de TODOS os grupos + qtd de grupos distintos)
            $totalReal = ['faturamento' => 0.0, 'qtd_notas' => 0, 'qtd_grupos' => 0, 'qtd_itens' => 0.0];
            try {
                $sqlTot = "
                    SELECT
                        COUNT(DISTINCT i.COD_ESTRUTURA)                              AS QTD_GRUPOS,
                        COUNT(DISTINCT nc.NR_NOTA || '|' || nc.SERIE || '|' ||
                                       nc.COD_TIPO_OPERACAO || '|' || nc.COD_EMPRESA) AS QTD_NOTAS,
                        COALESCE(SUM(ni.QTD), 0)                                     AS QTD_ITENS,
                        COALESCE(SUM(CAST(ni.QTD AS DOUBLE PRECISION) * ni.PRECO_UNIT), 0) AS FATURAMENTO
                    FROM NOTAS_CAB nc
                    JOIN NOTAS_ITENS ni
                         ON ni.NR_NOTA          = nc.NR_NOTA
                        AND ni.SERIE            = nc.SERIE
                        AND ni.COD_TIPO_OPERACAO = nc.COD_TIPO_OPERACAO
                        AND ni.COD_EMPRESA      = nc.COD_EMPRESA
                    LEFT JOIN ITENS i ON i.COD_ITEM = ni.COD_ITEM
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      {$fv}
                      {$fe}
                ";
                $rTot = $this->pdo->query($sqlTot)->fetch(\PDO::FETCH_ASSOC);
                if ($rTot) {
                    $totalReal = [
                        'faturamento' => (float)($rTot['FATURAMENTO'] ?? 0),
                        'qtd_notas'   => (int)($rTot['QTD_NOTAS'] ?? 0),
                        'qtd_grupos'  => (int)($rTot['QTD_GRUPOS'] ?? 0),
                        'qtd_itens'   => (float)($rTot['QTD_ITENS'] ?? 0),
                    ];
                }
            } catch (\Throwable) {}

            // Soma do Top N (pra calcular "outros")
            $sumTopFat   = 0.0; $sumTopNotas = 0; $sumTopItens = 0.0;
            foreach ($rows as $r) {
                $sumTopFat   += (float)$r['FATURAMENTO'];
                $sumTopNotas += (int)$r['QTD_NOTAS'];        // soma indicativa (notas podem se repetir entre grupos)
                $sumTopItens += (float)$r['QTD_ITENS'];
            }
            // Base de participação % = total REAL (não a soma do Top N) — leitura honesta.
            $totParticipacao = $totalReal['faturamento'] > 0 ? $totalReal['faturamento'] : $sumTopFat;

            $linhas = [];
            foreach ($rows as $r) {
                $fat   = (float)$r['FATURAMENTO'];
                $qtdN  = (int)$r['QTD_NOTAS'];
                $qtdI  = (float)$r['QTD_ITENS'];
                $linhas[] = [
                    'cod_grupo'         => $r['COD_GRUPO'] !== null ? (int)$r['COD_GRUPO'] : null,
                    'nome'              => (string)$r['DESCRICAO'],
                    'qtd_notas'         => $qtdN,
                    'qtd_itens'         => $qtdI,
                    'faturamento'       => $fat,
                    'ticket_medio'      => $qtdN > 0 ? $fat / $qtdN : 0.0,
                    'participacao_pct'  => $totParticipacao > 0 ? $fat / $totParticipacao * 100 : 0.0,
                ];
            }
            // Normaliza encoding Windows-1252 → UTF-8 no campo nome (DESCRICAO)
            foreach ($linhas as &$r) {
                if (!mb_check_encoding($r['nome'], 'UTF-8')) {
                    $r['nome'] = mb_convert_encoding($r['nome'], 'UTF-8', 'Windows-1252');
                }
            }
            unset($r);

            // (c) Linha "outros" = total - top N (só quando há corte real)
            $outros = null;
            $qtdGruposFora = max(0, $totalReal['qtd_grupos'] - count($rows));
            if ($qtdGruposFora > 0 && $totalReal['faturamento'] > $sumTopFat) {
                $fatFora = max(0.0, $totalReal['faturamento'] - $sumTopFat);
                $outros = [
                    'cod_grupo'        => null,
                    'nome'             => "Outros ({$qtdGruposFora} grupos)",
                    'qtd_notas'        => 0, // contagem distinta entre Top N e fora não é trivial
                    'qtd_itens'        => max(0.0, $totalReal['qtd_itens'] - $sumTopItens),
                    'faturamento'      => $fatFora,
                    'ticket_medio'     => 0.0,
                    'participacao_pct' => $totParticipacao > 0 ? $fatFora / $totParticipacao * 100 : 0.0,
                ];
            }

            return [
                'linhas'     => $linhas,
                'outros'     => $outros,
                'total_real' => $totalReal,
            ];
        });
    }

    /**
     * Retorna ranking de vendas por MARCA no período.
     * Mesma estrutura do retorno de vendasPorGrupo: ['linhas','outros','total_real'].
     */
    public function vendasPorMarca(string $dataIni, string $dataFim, int $limite = 50): array
    {
        $limite = max(1, min(500, $limite));
        $cacheKey = "vendas_por_marca_v2_{$dataIni}_{$dataFim}_{$limite}_" . (self::$codEmpresa ?? 'na');
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $limite) {
            $iv = $this->intervaloData($dataIni, $dataFim, 'nc');
            $fe = $this->filtroEmpresa('nc');
            $fv = $this->filtroVenda('nc');

            // (a) Top N detalhado
            $rows = [];
            try {
                $sql = "
                    SELECT FIRST {$limite}
                        i.COD_MARCA                                                  AS COD_MARCA,
                        TRIM(COALESCE(m.DESCRICAO, '(sem marca)'))                   AS DESCRICAO,
                        COUNT(DISTINCT nc.NR_NOTA || '|' || nc.SERIE || '|' ||
                                       nc.COD_TIPO_OPERACAO || '|' || nc.COD_EMPRESA) AS QTD_NOTAS,
                        COALESCE(SUM(ni.QTD), 0)                                     AS QTD_ITENS,
                        COALESCE(SUM(CAST(ni.QTD AS DOUBLE PRECISION) * ni.PRECO_UNIT), 0) AS FATURAMENTO
                    FROM NOTAS_CAB nc
                    JOIN NOTAS_ITENS ni
                         ON ni.NR_NOTA          = nc.NR_NOTA
                        AND ni.SERIE            = nc.SERIE
                        AND ni.COD_TIPO_OPERACAO = nc.COD_TIPO_OPERACAO
                        AND ni.COD_EMPRESA      = nc.COD_EMPRESA
                    LEFT JOIN ITENS i ON i.COD_ITEM   = ni.COD_ITEM
                    LEFT JOIN MARCA m ON m.COD_MARCA  = i.COD_MARCA
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      {$fv}
                      {$fe}
                    GROUP BY i.COD_MARCA, TRIM(COALESCE(m.DESCRICAO, '(sem marca)'))
                    ORDER BY FATURAMENTO DESC
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
            } catch (\Throwable) {
                $rows = [];
            }

            // (b) Agregado total REAL (universo completo, qtd de marcas distintas)
            $totalReal = ['faturamento' => 0.0, 'qtd_notas' => 0, 'qtd_marcas' => 0, 'qtd_itens' => 0.0];
            try {
                $sqlTot = "
                    SELECT
                        COUNT(DISTINCT i.COD_MARCA)                                  AS QTD_MARCAS,
                        COUNT(DISTINCT nc.NR_NOTA || '|' || nc.SERIE || '|' ||
                                       nc.COD_TIPO_OPERACAO || '|' || nc.COD_EMPRESA) AS QTD_NOTAS,
                        COALESCE(SUM(ni.QTD), 0)                                     AS QTD_ITENS,
                        COALESCE(SUM(CAST(ni.QTD AS DOUBLE PRECISION) * ni.PRECO_UNIT), 0) AS FATURAMENTO
                    FROM NOTAS_CAB nc
                    JOIN NOTAS_ITENS ni
                         ON ni.NR_NOTA          = nc.NR_NOTA
                        AND ni.SERIE            = nc.SERIE
                        AND ni.COD_TIPO_OPERACAO = nc.COD_TIPO_OPERACAO
                        AND ni.COD_EMPRESA      = nc.COD_EMPRESA
                    LEFT JOIN ITENS i ON i.COD_ITEM = ni.COD_ITEM
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      {$fv}
                      {$fe}
                ";
                $rTot = $this->pdo->query($sqlTot)->fetch(\PDO::FETCH_ASSOC);
                if ($rTot) {
                    $totalReal = [
                        'faturamento' => (float)($rTot['FATURAMENTO'] ?? 0),
                        'qtd_notas'   => (int)($rTot['QTD_NOTAS'] ?? 0),
                        'qtd_marcas'  => (int)($rTot['QTD_MARCAS'] ?? 0),
                        'qtd_itens'   => (float)($rTot['QTD_ITENS'] ?? 0),
                    ];
                }
            } catch (\Throwable) {}

            $sumTopFat = 0.0; $sumTopItens = 0.0;
            foreach ($rows as $r) {
                $sumTopFat   += (float)$r['FATURAMENTO'];
                $sumTopItens += (float)$r['QTD_ITENS'];
            }
            $totParticipacao = $totalReal['faturamento'] > 0 ? $totalReal['faturamento'] : $sumTopFat;

            $linhas = [];
            foreach ($rows as $r) {
                $fat   = (float)$r['FATURAMENTO'];
                $qtdN  = (int)$r['QTD_NOTAS'];
                $qtdI  = (float)$r['QTD_ITENS'];
                $linhas[] = [
                    'cod_marca'         => $r['COD_MARCA'] !== null ? (int)$r['COD_MARCA'] : null,
                    'nome'              => (string)$r['DESCRICAO'],
                    'qtd_notas'         => $qtdN,
                    'qtd_itens'         => $qtdI,
                    'faturamento'       => $fat,
                    'ticket_medio'      => $qtdN > 0 ? $fat / $qtdN : 0.0,
                    'participacao_pct'  => $totParticipacao > 0 ? $fat / $totParticipacao * 100 : 0.0,
                ];
            }
            foreach ($linhas as &$r) {
                if (!mb_check_encoding($r['nome'], 'UTF-8')) {
                    $r['nome'] = mb_convert_encoding($r['nome'], 'UTF-8', 'Windows-1252');
                }
            }
            unset($r);

            $outros = null;
            $qtdMarcasFora = max(0, $totalReal['qtd_marcas'] - count($rows));
            if ($qtdMarcasFora > 0 && $totalReal['faturamento'] > $sumTopFat) {
                $fatFora = max(0.0, $totalReal['faturamento'] - $sumTopFat);
                $outros = [
                    'cod_marca'        => null,
                    'nome'             => "Outros ({$qtdMarcasFora} marcas)",
                    'qtd_notas'        => 0,
                    'qtd_itens'        => max(0.0, $totalReal['qtd_itens'] - $sumTopItens),
                    'faturamento'      => $fatFora,
                    'ticket_medio'     => 0.0,
                    'participacao_pct' => $totParticipacao > 0 ? $fatFora / $totParticipacao * 100 : 0.0,
                ];
            }

            return [
                'linhas'     => $linhas,
                'outros'     => $outros,
                'total_real' => $totalReal,
            ];
        });
    }

    // -------------------------------------------------------------------------
    // Comparativo (período atual vs anterior) — base p/ insights "subiu/caiu"
    // -------------------------------------------------------------------------

    /**
     * Vendas por GRUPO com comparativo entre dois períodos.
     * Retorna ['linhas_com_var', 'top_crescimento', 'top_queda',
     *          'total_real_atual', 'total_real_anterior'].
     */
    public function vendasPorGrupoComparativo(string $de, string $ate, string $deAnterior, string $ateAnterior, int $limite = 50): array
    {
        $atual    = $this->vendasPorGrupo($de, $ate, $limite);
        // No anterior pegamos um Top alto pra maximizar a chance de "casar" a chave.
        $anterior = $this->vendasPorGrupo($deAnterior, $ateAnterior, 500);

        // Index anterior por cod_grupo (null vira chave especial '_sem_grupo')
        $idxAnt = [];
        foreach ($anterior['linhas'] as $r) {
            $k = $r['cod_grupo'] !== null ? (string)$r['cod_grupo'] : '_sem_grupo';
            $idxAnt[$k] = $r;
        }

        $linhasVar = [];
        $topCresc  = null;
        $topQueda  = null;
        foreach ($atual['linhas'] as $r) {
            $k = $r['cod_grupo'] !== null ? (string)$r['cod_grupo'] : '_sem_grupo';
            $fatAnt = isset($idxAnt[$k]) ? (float)$idxAnt[$k]['faturamento'] : 0.0;
            $fatAtu = (float)$r['faturamento'];
            $tinhaAntes = isset($idxAnt[$k]);
            $varPct = null;
            if ($tinhaAntes && $fatAnt > 0) {
                $varPct = ($fatAtu - $fatAnt) / $fatAnt * 100;
            } elseif ($tinhaAntes && $fatAnt == 0 && $fatAtu > 0) {
                $varPct = null; // base zero — não dá pra calcular %
            }
            $linha = $r + [
                'faturamento_anterior' => $fatAnt,
                'var_pct'              => $varPct,
                'novo'                 => !$tinhaAntes,
            ];
            $linhasVar[] = $linha;

            if ($varPct !== null) {
                if ($topCresc === null || $varPct > ($topCresc['var_pct'] ?? -INF)) {
                    $topCresc = ['nome' => $r['nome'], 'var_pct' => $varPct, 'atual' => $fatAtu, 'anterior' => $fatAnt, 'cod_grupo' => $r['cod_grupo']];
                }
                if ($topQueda === null || $varPct < ($topQueda['var_pct'] ?? INF)) {
                    $topQueda = ['nome' => $r['nome'], 'var_pct' => $varPct, 'atual' => $fatAtu, 'anterior' => $fatAnt, 'cod_grupo' => $r['cod_grupo']];
                }
            }
        }

        return [
            'linhas_com_var'      => $linhasVar,
            'top_crescimento'     => $topCresc,
            'top_queda'           => $topQueda,
            'total_real_atual'    => $atual['total_real']    ?? null,
            'total_real_anterior' => $anterior['total_real'] ?? null,
        ];
    }

    /**
     * Vendas por MARCA com comparativo entre dois períodos.
     * Mesma estrutura de vendasPorGrupoComparativo.
     */
    public function vendasPorMarcaComparativo(string $de, string $ate, string $deAnterior, string $ateAnterior, int $limite = 50): array
    {
        $atual    = $this->vendasPorMarca($de, $ate, $limite);
        $anterior = $this->vendasPorMarca($deAnterior, $ateAnterior, 500);

        $idxAnt = [];
        foreach ($anterior['linhas'] as $r) {
            $k = $r['cod_marca'] !== null ? (string)$r['cod_marca'] : '_sem_marca';
            $idxAnt[$k] = $r;
        }

        $linhasVar = [];
        $topCresc  = null;
        $topQueda  = null;
        foreach ($atual['linhas'] as $r) {
            $k = $r['cod_marca'] !== null ? (string)$r['cod_marca'] : '_sem_marca';
            $fatAnt = isset($idxAnt[$k]) ? (float)$idxAnt[$k]['faturamento'] : 0.0;
            $fatAtu = (float)$r['faturamento'];
            $tinhaAntes = isset($idxAnt[$k]);
            $varPct = null;
            if ($tinhaAntes && $fatAnt > 0) {
                $varPct = ($fatAtu - $fatAnt) / $fatAnt * 100;
            }
            $linha = $r + [
                'faturamento_anterior' => $fatAnt,
                'var_pct'              => $varPct,
                'novo'                 => !$tinhaAntes,
            ];
            $linhasVar[] = $linha;

            if ($varPct !== null) {
                if ($topCresc === null || $varPct > ($topCresc['var_pct'] ?? -INF)) {
                    $topCresc = ['nome' => $r['nome'], 'var_pct' => $varPct, 'atual' => $fatAtu, 'anterior' => $fatAnt, 'cod_marca' => $r['cod_marca']];
                }
                if ($topQueda === null || $varPct < ($topQueda['var_pct'] ?? INF)) {
                    $topQueda = ['nome' => $r['nome'], 'var_pct' => $varPct, 'atual' => $fatAtu, 'anterior' => $fatAnt, 'cod_marca' => $r['cod_marca']];
                }
            }
        }

        return [
            'linhas_com_var'      => $linhasVar,
            'top_crescimento'     => $topCresc,
            'top_queda'           => $topQueda,
            'total_real_atual'    => $atual['total_real']    ?? null,
            'total_real_anterior' => $anterior['total_real'] ?? null,
        ];
    }

    // -------------------------------------------------------------------------
    // Drill-down: produtos vendidos dentro de UM grupo / UMA marca específica
    // -------------------------------------------------------------------------

    /** Top N produtos vendidos NO grupo $codGrupo durante o período. */
    public function produtosPorGrupo(int $codGrupo, string $dataIni, string $dataFim, int $limite = 20): array
    {
        $limite   = max(1, min(500, $limite));
        $cacheKey = "vendas_prod_por_grupo_{$codGrupo}_{$dataIni}_{$dataFim}_{$limite}_" . (self::$codEmpresa ?? 'na');
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($codGrupo, $dataIni, $dataFim, $limite) {
            $iv = $this->intervaloData($dataIni, $dataFim, 'nc');
            $fe = $this->filtroEmpresa('nc');
            $fv = $this->filtroVenda('nc');

            try {
                $sql = "
                    SELECT FIRST {$limite}
                        ni.COD_ITEM                                                  AS COD_ITEM,
                        TRIM(MAX(COALESCE(i.DESCRICAO, ni.DESCRICAO, '(sem descrição)'))) AS DESCRICAO,
                        COALESCE(SUM(ni.QTD), 0)                                     AS QTD,
                        COALESCE(SUM(CAST(ni.QTD AS DOUBLE PRECISION) * ni.PRECO_UNIT), 0) AS FATURAMENTO
                    FROM NOTAS_CAB nc
                    JOIN NOTAS_ITENS ni
                         ON ni.NR_NOTA          = nc.NR_NOTA
                        AND ni.SERIE            = nc.SERIE
                        AND ni.COD_TIPO_OPERACAO = nc.COD_TIPO_OPERACAO
                        AND ni.COD_EMPRESA      = nc.COD_EMPRESA
                    JOIN ITENS i ON i.COD_ITEM = ni.COD_ITEM
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      AND i.COD_ESTRUTURA = {$codGrupo}
                      {$fv}
                      {$fe}
                    GROUP BY ni.COD_ITEM
                    ORDER BY FATURAMENTO DESC
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
            } catch (\Throwable) {
                return [];
            }

            $out = [];
            foreach ($rows as $r) {
                $desc = (string)$r['DESCRICAO'];
                if (!mb_check_encoding($desc, 'UTF-8')) {
                    $desc = mb_convert_encoding($desc, 'UTF-8', 'Windows-1252');
                }
                $out[] = [
                    'cod_item'    => (int)$r['COD_ITEM'],
                    'descricao'   => $desc,
                    'qtd'         => (float)$r['QTD'],
                    'faturamento' => (float)$r['FATURAMENTO'],
                ];
            }
            return $out;
        });
    }

    /** Top N produtos vendidos da marca $codMarca durante o período. */
    public function produtosPorMarca(int $codMarca, string $dataIni, string $dataFim, int $limite = 20): array
    {
        $limite   = max(1, min(500, $limite));
        $cacheKey = "vendas_prod_por_marca_{$codMarca}_{$dataIni}_{$dataFim}_{$limite}_" . (self::$codEmpresa ?? 'na');
        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($codMarca, $dataIni, $dataFim, $limite) {
            $iv = $this->intervaloData($dataIni, $dataFim, 'nc');
            $fe = $this->filtroEmpresa('nc');
            $fv = $this->filtroVenda('nc');

            try {
                $sql = "
                    SELECT FIRST {$limite}
                        ni.COD_ITEM                                                  AS COD_ITEM,
                        TRIM(MAX(COALESCE(i.DESCRICAO, ni.DESCRICAO, '(sem descrição)'))) AS DESCRICAO,
                        COALESCE(SUM(ni.QTD), 0)                                     AS QTD,
                        COALESCE(SUM(CAST(ni.QTD AS DOUBLE PRECISION) * ni.PRECO_UNIT), 0) AS FATURAMENTO
                    FROM NOTAS_CAB nc
                    JOIN NOTAS_ITENS ni
                         ON ni.NR_NOTA          = nc.NR_NOTA
                        AND ni.SERIE            = nc.SERIE
                        AND ni.COD_TIPO_OPERACAO = nc.COD_TIPO_OPERACAO
                        AND ni.COD_EMPRESA      = nc.COD_EMPRESA
                    JOIN ITENS i ON i.COD_ITEM = ni.COD_ITEM
                    WHERE {$iv}
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      AND i.COD_MARCA = {$codMarca}
                      {$fv}
                      {$fe}
                    GROUP BY ni.COD_ITEM
                    ORDER BY FATURAMENTO DESC
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
            } catch (\Throwable) {
                return [];
            }

            $out = [];
            foreach ($rows as $r) {
                $desc = (string)$r['DESCRICAO'];
                if (!mb_check_encoding($desc, 'UTF-8')) {
                    $desc = mb_convert_encoding($desc, 'UTF-8', 'Windows-1252');
                }
                $out[] = [
                    'cod_item'    => (int)$r['COD_ITEM'],
                    'descricao'   => $desc,
                    'qtd'         => (float)$r['QTD'],
                    'faturamento' => (float)$r['FATURAMENTO'],
                ];
            }
            return $out;
        });
    }
}
