<?php
// =============================================================================
// Depto de Engenharia e Automacao - Master Automacao - Autor: Derlivon Silva
// =============================================================================

namespace App\Repositories;

use App\Core\Cache;

/**
 * Consultas SQL do Dashboard Executivo contra o Master ERP (Firebird).
 * Usa uma data de referência dinâmica: último dia com vendas no banco,
 * permitindo operar corretamente com bancos de demonstração ou históricos.
 */
class DashboardRepository
{
    private string $dataRef;   // 'YYYY-MM-DD' — referência para "hoje"
    private int    $mesRef;
    private int    $anoRef;
    private bool   $eHoje;     // true se o banco tem dados de CURRENT_DATE
    private int    $codEmpresa;

    // Intervalo do mês de referência (calculado uma vez)
    private string $mesIni;
    private string $mesFim;

    // Cláusulas WHERE para filtrar empresa (vazias se codEmpresa=0)
    private string $wEmp;     // " AND COD_EMPRESA = X"
    private string $wEmpNc;   // " AND nc.COD_EMPRESA = X"
    private string $wEmpSolo; // "WHERE COD_EMPRESA = X" ou "" (query sem outros filtros)

    // Filtro de VENDAS reais (exclui compras, transferências, devoluções, mov. interno)
    // — mesma lógica do VendasRepository, p/ o faturamento bater entre Dashboard,
    // Consolidado e Gestão de Vendas. " AND COD_TIPO_OPERACAO IN (...)" ou "".
    private string $wVenda = '';

    public function __construct(private \PDO $pdo, ?string $dataOverride = null, int $codEmpresa = 0)
    {
        $this->codEmpresa = $codEmpresa;
        $this->wEmp     = $codEmpresa > 0 ? " AND COD_EMPRESA = {$codEmpresa}" : '';
        $this->wEmpNc   = $codEmpresa > 0 ? " AND nc.COD_EMPRESA = {$codEmpresa}" : '';
        $this->wEmpSolo = $codEmpresa > 0 ? " WHERE COD_EMPRESA = {$codEmpresa}" : '';
        $this->wVenda   = $this->detectarFiltroVenda();

        $this->dataRef = $dataOverride ?? $this->detectarUltimaData();
        $this->mesRef  = (int) substr($this->dataRef, 5, 2);
        $this->anoRef  = (int) substr($this->dataRef, 0, 4);
        $this->eHoje   = ($this->dataRef === date('Y-m-d'));

        // Pré-calcula intervalo do mês (evita EXTRACT no WHERE)
        $this->mesIni = "{$this->anoRef}-" . str_pad($this->mesRef, 2, '0', STR_PAD_LEFT) . "-01";
        $this->mesFim = date('Y-m-t', strtotime($this->mesIni));
    }

    /**
     * Monta " AND COD_TIPO_OPERACAO IN (...)" com as operações de VENDA real
     * (natureza 'V' menos devoluções/transferências/movimentos internos) — mesma
     * regra do VendasRepository, p/ o faturamento do Dashboard/Consolidado bater
     * com a Gestão de Vendas (antes somava TODAS as notas, inclusive compras).
     * Fallback: '' (sem filtro) se TIPO_OPERACAO não existir.
     */
    private function detectarFiltroVenda(): string
    {
        // PK de TIPO_OPERACAO é (COD_TIPO_OPERACAO, COD_EMPRESA): cada empresa tem o seu
        // cadastro. Filtra pela empresa ATIVA pra não pegar flag de outra. Sem empresa, global.
        $emp = $this->codEmpresa ?: 0;
        $empWhere = $emp > 0 ? " 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){$empWhere}
              )";
        $rowsOK = null;
        foreach (["OR GERA_FINANCEIRO = 'N'", ''] as $extra) {
            try {
                $rowsOK = $this->pdo->query(sprintf($sqlBase, $extra))->fetchAll(\PDO::FETCH_COLUMN);
                break;
            } catch (\Throwable) {}
        }
        $ops = $rowsOK ? array_map('intval', $rowsOK) : [];

        // === OVERRIDES por cliente (config/database.php) ===
        //   'tipos_venda_extras'  => [115, 168, ...]  // INCLUI estas ops
        //   'tipos_venda_excluir' => [291, ...]        // EXCLUI estas
        $cfg = $this->configCliente();
        foreach ((array)($cfg['tipos_venda_extras']  ?? []) as $e)
            if (!in_array((int)$e, $ops, true)) $ops[] = (int)$e;
        $ops = array_values(array_diff($ops, array_map('intval', (array)($cfg['tipos_venda_excluir'] ?? []))));

        if (empty($ops)) return '';
        return ' AND COD_TIPO_OPERACAO IN (' . implode(',', $ops) . ')';
    }

    /** Lê config/database.php uma vez por request. */
    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;
    }

    // -------------------------------------------------------------------------
    // Data de referência
    // -------------------------------------------------------------------------

    public function dataReferencia(): string  { return $this->dataRef; }
    public function mesReferencia(): int      { return $this->mesRef;  }
    public function anoReferencia(): int      { return $this->anoRef;  }
    public function eHoje(): bool             { return $this->eHoje;   }

    private function detectarUltimaData(): string
    {
        $key = 'dash_ultima_data_' . $this->codEmpresa;
        return Cache::lembrar($key, 600, function () {
            try {
                // MAX só na JANELA RECENTE (últimos 120 dias): usa o índice ascendente p/ o
                // range → instantâneo. Buscar o MAX sem limite custa ~45s no Firebird 2.5
                // (sem índice descendente). Ignora data no futuro (registro furado).
                $ultima = $this->escalar("
                    SELECT MAX(DATA_EMISSAO)
                    FROM NOTAS_CAB
                    WHERE DATA_CANC IS NULL
                      AND DATA_EMISSAO >= DATEADD(DAY, -120, CURRENT_DATE)
                      AND DATA_EMISSAO <= CURRENT_DATE
                      {$this->wEmp}
                ");
                if ($ultima) {
                    if ($ultima instanceof \DateTime) return $ultima->format('Y-m-d');
                    $s = substr(trim((string)$ultima), 0, 10);
                    return ($s > date('Y-m-d')) ? date('Y-m-d') : $s;
                }
            } catch (\Throwable) {}
            return date('Y-m-d');
        });
    }

    /**
     * Último dia com dados em um ano — UMA query em vez de 12.
     * Retorna ['data' => 'YYYY-MM-DD', 'mes' => N] ou null.
     */
    public function ultimoDiaDoAno(int $ano): ?array
    {
        try {
            $ini = "{$ano}-01-01";
            $fim = "{$ano}-12-31";
            $row = $this->pdo->query("
                SELECT FIRST 1 DATA_EMISSAO
                FROM NOTAS_CAB
                WHERE DATA_CANC IS NULL
                  AND DATA_EMISSAO BETWEEN '{$ini}' AND '{$fim}'
                  {$this->wEmp}
                ORDER BY DATA_EMISSAO DESC
            ")->fetch(\PDO::FETCH_ASSOC);

            if ($row) {
                $data = $row['DATA_EMISSAO'];
                if ($data instanceof \DateTime) $data = $data->format('Y-m-d');
                else $data = substr(trim((string)$data), 0, 10);
                return ['data' => $data, 'mes' => (int)substr($data, 5, 2)];
            }
        } catch (\Throwable) {}
        return null;
    }

    /** Último dia com dados dentro de um mês/ano específico */
    public function ultimoDiaDoMes(int $ano, int $mes): ?string
    {
        try {
            $ini = "{$ano}-" . str_pad($mes, 2, '0', STR_PAD_LEFT) . "-01";
            $fim = date('Y-m-t', strtotime($ini));
            $val = $this->escalar("
                SELECT FIRST 1 DATA_EMISSAO
                FROM NOTAS_CAB
                WHERE DATA_CANC IS NULL
                  AND DATA_EMISSAO BETWEEN '{$ini}' AND '{$fim}'
                  {$this->wEmp}
                ORDER BY DATA_EMISSAO DESC
            ");
            if ($val) {
                if ($val instanceof \DateTime) return $val->format('Y-m-d');
                return substr(trim((string)$val), 0, 10);
            }
        } catch (\Throwable) {}
        return null;
    }

    /** Anos disponíveis — últimos 5 anos.
     *  USA FIRST 1 ORDER BY (índice em DATA_EMISSAO) em vez de DISTINCT scan.
     *  DISTINCT obriga Firebird a ler todos os 1.3M+ rows → 35s. */
    public function anosDisponiveis(): array
    {
        $key = 'dash_anos_' . $this->codEmpresa;
        return Cache::lembrar($key, 3600, function () {
            try {
                $anoAtual = (int)date('Y');
                $anoIni   = $anoAtual - 4;
                $ini = "{$anoIni}-01-01";
                $fim = date('Y-m-d');

                // FIRST 1 ORDER BY usa índice → O(log n) em vez de full scan
                $minAno = (int)$this->escalar("
                    SELECT FIRST 1 EXTRACT(YEAR FROM DATA_EMISSAO)
                    FROM NOTAS_CAB
                    WHERE DATA_EMISSAO >= '{$ini}'
                      {$this->wEmp}
                    ORDER BY DATA_EMISSAO ASC
                ");
                $maxAno = (int)$this->escalar("
                    SELECT FIRST 1 EXTRACT(YEAR FROM DATA_EMISSAO)
                    FROM NOTAS_CAB
                    WHERE DATA_EMISSAO <= '{$fim}'
                      {$this->wEmp}
                    ORDER BY DATA_EMISSAO DESC
                ");

                if (!$minAno || !$maxAno) return [$anoAtual];

                // Gera lista de anos em PHP sem varredura adicional
                $anos = [];
                for ($a = $maxAno; $a >= max($minAno, $anoIni); $a--) {
                    $anos[] = $a;
                }
                return $anos ?: [$anoAtual];
            } catch (\Throwable) {
                return [(int)date('Y')];
            }
        });
    }

    /** Meses com dados em um ano — gera via PHP (FIRST 1 por mês = 12 queries rápidas,
     *  melhor que DISTINCT que faz full scan do ano inteiro). */
    public function mesesDisponiveis(int $ano): array
    {
        $key = "dash_meses_{$ano}_{$this->codEmpresa}";
        return Cache::lembrar($key, 3600, function () use ($ano) {
            try {
                $meses = [];
                $hoje = date('Y-m-d');
                for ($m = 1; $m <= 12; $m++) {
                    $ini = "{$ano}-" . str_pad($m, 2, '0', STR_PAD_LEFT) . "-01";
                    if ($ini > $hoje) break; // não verifica meses futuros
                    $fim = date('Y-m-t', strtotime($ini));
                    // FIRST 1 BETWEEN = O(log n) pelo índice
                    $v = $this->escalar("
                        SELECT FIRST 1 EXTRACT(MONTH FROM DATA_EMISSAO)
                        FROM NOTAS_CAB
                        WHERE DATA_EMISSAO BETWEEN '{$ini}' AND '{$fim}'
                          {$this->wEmp}
                        ORDER BY DATA_EMISSAO ASC
                    ");
                    if ($v !== null && $v !== false && $v !== 0) {
                        $meses[] = $m;
                    }
                }
                return $meses;
            } catch (\Throwable) { return []; }
        });
    }

    // -------------------------------------------------------------------------
    // Faturamento
    // -------------------------------------------------------------------------

    public function faturamentoHoje(): float
    {
        $ttl = $this->eHoje ? 60 : 1800;
        $key = "dash_fat_hoje_v_{$this->dataRef}_{$this->codEmpresa}";
        return (float) Cache::lembrar($key, $ttl, fn() => $this->escalar("
            SELECT COALESCE(SUM(TOTAL_NOTA), 0)
            FROM NOTAS_CAB
            WHERE DATA_EMISSAO = '{$this->dataRef}'
              AND DATA_CANC IS NULL
              {$this->wEmp}{$this->wVenda}
        "));
    }

    public function faturamentoMes(): float
    {
        $ttl = $this->eHoje ? 60 : 1800;
        $key = "dash_fat_mes_v_{$this->mesIni}_{$this->codEmpresa}";
        return (float) Cache::lembrar($key, $ttl, fn() => $this->escalar("
            SELECT COALESCE(SUM(TOTAL_NOTA), 0)
            FROM NOTAS_CAB
            WHERE DATA_EMISSAO BETWEEN '{$this->mesIni}' AND '{$this->mesFim}'
              AND DATA_CANC IS NULL
              {$this->wEmp}{$this->wVenda}
        "));
    }

    /** Faturamento de VENDAS num intervalo qualquer [ini,fim] (p/ consolidado hoje/semana/mês/ano). */
    public function faturamentoIntervalo(string $ini, string $fim): float
    {
        $ttl = ($fim >= date('Y-m-d')) ? 60 : 1800;
        $key = "dash_fat_int_v_{$ini}_{$fim}_{$this->codEmpresa}";
        return (float) Cache::lembrar($key, $ttl, fn() => $this->escalar("
            SELECT COALESCE(SUM(TOTAL_NOTA), 0)
            FROM NOTAS_CAB
            WHERE DATA_EMISSAO >= '{$ini}' AND DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$fim}' AS DATE))
              AND DATA_CANC IS NULL
              {$this->wEmp}{$this->wVenda}
        "));
    }

    /**
     * Faturamento "Com Nota" vs "Sem Nota" num intervalo qualquer [ini,fim].
     *
     * Classificação por FLAGS do cadastro da NOTAS_CAB (mesmo CASE do
     * VendasRepository::faturamentoFiscal — manter paridade entre telas):
     *   ENFCE='S' OR NFE='S'  -> COM_NOTA (NFC-e modelo 65 OU NF-e modelo 55)
     *   senão                 -> SEM_NOTA (venda sem documento fiscal)
     *
     * Restrito a VENDAS reais (mesmo filtroVenda das demais queries do dashboard)
     * + DATA_CANC IS NULL + filtro de empresa quando ativo. 1 query agregada com
     * COUNT/SUM via CASE — sem segunda viagem ao banco.
     *
     * Retorno:
     *   [
     *     'com_nota'      => ['qtd' => int, 'valor' => float],
     *     'sem_nota'      => ['qtd' => int, 'valor' => float],
     *     'total'         => ['qtd' => int, 'valor' => float],
     *     'pct_com_nota'  => float (0–100, valor de COM_NOTA / valor total)
     *   ]
     *
     * Fallback: se a coluna ENFCE/NFE não existir (bancos antigos do Fire),
     * tenta classificar pela SERIE ('SC' => COM_NOTA, demais => SEM_NOTA).
     * Em caso de erro total, devolve zeros silenciosamente (igual aos demais
     * métodos do dashboard — não derruba a tela do consolidado).
     */
    public function faturamentoComSemNota(string $dataIni, string $dataFim): array
    {
        $ttl = ($dataFim >= date('Y-m-d')) ? 60 : 1800;
        $key = "dash_csn_v_{$dataIni}_{$dataFim}_{$this->codEmpresa}";

        return Cache::lembrar($key, $ttl, function () use ($dataIni, $dataFim) {
            $zeros = [
                'com_nota'     => ['qtd' => 0, 'valor' => 0.0],
                'sem_nota'     => ['qtd' => 0, 'valor' => 0.0],
                'total'        => ['qtd' => 0, 'valor' => 0.0],
                'pct_com_nota' => 0.0,
            ];

            // Tentativa 1: pelas FLAGS reais (ENFCE/NFE) — caminho moderno.
            try {
                $sql = "
                    SELECT
                        SUM(CASE WHEN UPPER(TRIM(COALESCE(ENFCE, ''))) = 'S'
                                   OR UPPER(TRIM(COALESCE(NFE,   ''))) = 'S'
                                 THEN 1 ELSE 0 END)                              AS QTD_COM,
                        SUM(CASE WHEN UPPER(TRIM(COALESCE(ENFCE, ''))) = 'S'
                                   OR UPPER(TRIM(COALESCE(NFE,   ''))) = 'S'
                                 THEN TOTAL_NOTA ELSE 0 END)                     AS VAL_COM,
                        SUM(CASE WHEN UPPER(TRIM(COALESCE(ENFCE, ''))) <> 'S'
                                  AND UPPER(TRIM(COALESCE(NFE,   ''))) <> 'S'
                                 THEN 1 ELSE 0 END)                              AS QTD_SEM,
                        SUM(CASE WHEN UPPER(TRIM(COALESCE(ENFCE, ''))) <> 'S'
                                  AND UPPER(TRIM(COALESCE(NFE,   ''))) <> 'S'
                                 THEN TOTAL_NOTA ELSE 0 END)                     AS VAL_SEM
                    FROM NOTAS_CAB
                    WHERE DATA_EMISSAO >= '{$dataIni}'
                      AND DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$dataFim}' AS DATE))
                      AND DATA_CANC IS NULL
                      {$this->wEmp}{$this->wVenda}
                ";
                $row = $this->pdo->query($sql)->fetch(\PDO::FETCH_ASSOC);
                if ($row) {
                    return $this->montarComSemNota(
                        (int)   ($row['QTD_COM'] ?? 0),
                        (float) ($row['VAL_COM'] ?? 0),
                        (int)   ($row['QTD_SEM'] ?? 0),
                        (float) ($row['VAL_SEM'] ?? 0),
                    );
                }
            } catch (\Throwable) {
                // cai pro fallback
            }

            // Tentativa 2: fallback pela SERIE (bancos sem coluna ENFCE/NFE).
            //   SERIE = 'SC' -> NFC-e (com nota); demais -> sem nota.
            try {
                $sql = "
                    SELECT
                        SUM(CASE WHEN UPPER(TRIM(COALESCE(SERIE, ''))) = 'SC'
                                 THEN 1 ELSE 0 END)                              AS QTD_COM,
                        SUM(CASE WHEN UPPER(TRIM(COALESCE(SERIE, ''))) = 'SC'
                                 THEN TOTAL_NOTA ELSE 0 END)                     AS VAL_COM,
                        SUM(CASE WHEN UPPER(TRIM(COALESCE(SERIE, ''))) <> 'SC'
                                 THEN 1 ELSE 0 END)                              AS QTD_SEM,
                        SUM(CASE WHEN UPPER(TRIM(COALESCE(SERIE, ''))) <> 'SC'
                                 THEN TOTAL_NOTA ELSE 0 END)                     AS VAL_SEM
                    FROM NOTAS_CAB
                    WHERE DATA_EMISSAO >= '{$dataIni}'
                      AND DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$dataFim}' AS DATE))
                      AND DATA_CANC IS NULL
                      {$this->wEmp}{$this->wVenda}
                ";
                $row = $this->pdo->query($sql)->fetch(\PDO::FETCH_ASSOC);
                if ($row) {
                    return $this->montarComSemNota(
                        (int)   ($row['QTD_COM'] ?? 0),
                        (float) ($row['VAL_COM'] ?? 0),
                        (int)   ($row['QTD_SEM'] ?? 0),
                        (float) ($row['VAL_SEM'] ?? 0),
                    );
                }
            } catch (\Throwable) {
                // segue pro zero silencioso
            }

            return $zeros;
        });
    }

    /** Monta o array de retorno padronizado de faturamentoComSemNota(). */
    private function montarComSemNota(int $qtdCom, float $valCom, int $qtdSem, float $valSem): array
    {
        $qtdTot = $qtdCom + $qtdSem;
        $valTot = $valCom + $valSem;
        $pct    = $valTot > 0 ? ($valCom / $valTot) * 100.0 : 0.0;
        return [
            'com_nota'     => ['qtd' => $qtdCom, 'valor' => $valCom],
            'sem_nota'     => ['qtd' => $qtdSem, 'valor' => $valSem],
            'total'        => ['qtd' => $qtdTot, 'valor' => $valTot],
            'pct_com_nota' => $pct,
        ];
    }

    /** Última data com venda (janela 120d, p/ FB2.5). Usado p/ avisar base desatualizada. */
    public function ultimaVenda(): ?string
    {
        return Cache::lembrar("dash_ult_venda_v_{$this->codEmpresa}", 1800, function () {
            try {
                $r = $this->escalar("SELECT MAX(DATA_EMISSAO) FROM NOTAS_CAB
                    WHERE DATA_CANC IS NULL
                      AND DATA_EMISSAO >= DATEADD(DAY,-120,CURRENT_DATE) AND DATA_EMISSAO <= CURRENT_DATE
                      {$this->wEmp}{$this->wVenda}");
                if (!$r) return null;
                return $r instanceof \DateTime ? $r->format('Y-m-d') : substr((string)$r, 0, 10);
            } catch (\Throwable) { return null; }
        });
    }

    /** Quantidade de VENDAS num intervalo qualquer [ini,fim]. */
    public function totalVendasIntervalo(string $ini, string $fim): int
    {
        $ttl = ($fim >= date('Y-m-d')) ? 60 : 1800;
        $key = "dash_qtd_int_v_{$ini}_{$fim}_{$this->codEmpresa}";
        return (int) Cache::lembrar($key, $ttl, fn() => $this->escalar("
            SELECT COUNT(*)
            FROM NOTAS_CAB
            WHERE DATA_EMISSAO >= '{$ini}' AND DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$fim}' AS DATE))
              AND DATA_CANC IS NULL
              {$this->wEmp}{$this->wVenda}
        "));
    }

    public function ticketMedioHoje(): float
    {
        $ttl = $this->eHoje ? 60 : 1800;
        $key = "dash_ticket_v_{$this->dataRef}_{$this->codEmpresa}";
        return (float) Cache::lembrar($key, $ttl, fn() => $this->escalar("
            SELECT COALESCE(AVG(TOTAL_NOTA), 0)
            FROM NOTAS_CAB
            WHERE DATA_EMISSAO = '{$this->dataRef}'
              AND DATA_CANC IS NULL
              AND TOTAL_NOTA > 0
              {$this->wEmp}{$this->wVenda}
        "));
    }

    public function totalVendasHoje(): int
    {
        $ttl = $this->eHoje ? 60 : 1800;
        $key = "dash_qtd_hoje_v_{$this->dataRef}_{$this->codEmpresa}";
        return (int) Cache::lembrar($key, $ttl, fn() => $this->escalar("
            SELECT COUNT(*)
            FROM NOTAS_CAB
            WHERE DATA_EMISSAO = '{$this->dataRef}'
              AND DATA_CANC IS NULL
              {$this->wEmp}{$this->wVenda}
        "));
    }

    /** Faturamento do dia anterior ao dia de referência */
    public function faturamentoOntem(): float
    {
        $ontem = date('Y-m-d', strtotime($this->dataRef . ' -1 day'));
        $key   = "dash_fat_ontem_v_{$ontem}_{$this->codEmpresa}";
        return (float) Cache::lembrar($key, 1800, fn() => $this->escalar("
            SELECT COALESCE(SUM(TOTAL_NOTA), 0)
            FROM NOTAS_CAB
            WHERE DATA_EMISSAO = '{$ontem}'
              AND DATA_CANC IS NULL
              {$this->wEmp}{$this->wVenda}
        "));
    }

    /** Faturamento do mês anterior ao mês de referência */
    public function faturamentoMesAnterior(): float
    {
        $iniAnt = date('Y-m-01', strtotime($this->mesIni . ' -1 month'));
        $fimAnt = date('Y-m-t',  strtotime($iniAnt));
        $key    = "dash_fat_mes_ant_v_{$iniAnt}_{$this->codEmpresa}";
        return (float) Cache::lembrar($key, 3600, fn() => $this->escalar("
            SELECT COALESCE(SUM(TOTAL_NOTA), 0)
            FROM NOTAS_CAB
            WHERE DATA_EMISSAO BETWEEN '{$iniAnt}' AND '{$fimAnt}'
              AND DATA_CANC IS NULL
              {$this->wEmp}{$this->wVenda}
        "));
    }

    /** Faturamento do mesmo mês no ano anterior */
    public function faturamentoMesmoMesAnoAnterior(): float
    {
        $anoAnt = $this->anoRef - 1;
        $mesStr = str_pad($this->mesRef, 2, '0', STR_PAD_LEFT);
        $iniAnt = "{$anoAnt}-{$mesStr}-01";
        $fimAnt = date('Y-m-t', strtotime($iniAnt));
        $key    = "dash_fat_mes_ano_ant_v_{$iniAnt}_{$this->codEmpresa}";
        return (float) Cache::lembrar($key, 3600, fn() => $this->escalar("
            SELECT COALESCE(SUM(TOTAL_NOTA), 0)
            FROM NOTAS_CAB
            WHERE DATA_EMISSAO BETWEEN '{$iniAnt}' AND '{$fimAnt}'
              AND DATA_CANC IS NULL
              {$this->wEmp}{$this->wVenda}
        "));
    }

    /** Total de notas emitidas no mês de referência */
    public function totalVendasMes(): int
    {
        $ttl = $this->eHoje ? 60 : 1800;
        $key = "dash_qtd_mes_v_{$this->mesIni}_{$this->codEmpresa}";
        return (int) Cache::lembrar($key, $ttl, fn() => $this->escalar("
            SELECT COUNT(*)
            FROM NOTAS_CAB
            WHERE DATA_EMISSAO BETWEEN '{$this->mesIni}' AND '{$this->mesFim}'
              AND DATA_CANC IS NULL
              {$this->wEmp}{$this->wVenda}
        "));
    }

    /** Clientes distintos que compraram no mês de referência */
    public function clientesAtivosMes(): int
    {
        $ttl = $this->eHoje ? 60 : 1800;
        $key = "dash_cli_ativos_{$this->mesIni}_{$this->codEmpresa}";
        return (int) Cache::lembrar($key, $ttl, fn() => $this->escalar("
            SELECT COUNT(DISTINCT COD_PESSOA)
            FROM NOTAS_CAB
            WHERE DATA_EMISSAO BETWEEN '{$this->mesIni}' AND '{$this->mesFim}'
              AND DATA_CANC IS NULL
              AND COD_PESSOA IS NOT NULL
              {$this->wEmp}
        "));
    }

    /** Melhor vendedor do mês de referência */
    public function melhorVendedorMes(): array
    {
        $ttl = $this->eHoje ? 60 : 1800;
        $key = "dash_melhor_vend_{$this->mesIni}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () {
            try {
                $row = $this->pdo->query("
                    SELECT FIRST 1
                        COALESCE(TRIM(p.NOME), 'Sem nome')  AS NOME,
                        COUNT(nc.NR_NOTA)                    AS QTD,
                        COALESCE(SUM(nc.TOTAL_NOTA), 0)     AS TOTAL
                    FROM NOTAS_CAB nc
                    LEFT JOIN PESSOAS p ON p.COD_PESSOA = nc.COD_PESSOA_VENDA
                    WHERE nc.DATA_EMISSAO BETWEEN '{$this->mesIni}' AND '{$this->mesFim}'
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      AND nc.COD_PESSOA_VENDA IS NOT NULL
                      {$this->wEmp}
                    GROUP BY COALESCE(TRIM(p.NOME), 'Sem nome')
                    ORDER BY TOTAL DESC
                ")->fetch(\PDO::FETCH_ASSOC);
                return $row ?: ['NOME' => '—', 'TOTAL' => 0, 'QTD' => 0];
            } catch (\Throwable) {
                return ['NOME' => '—', 'TOTAL' => 0, 'QTD' => 0];
            }
        });
    }

    // -------------------------------------------------------------------------
    // Período histórico — com cache
    // -------------------------------------------------------------------------

    /** Dias com maior faturamento — últimos 2 anos (BETWEEN usa índice).
     *  TTL de 24h: dados históricos mudam pouco; evitar cold scan frequente. */
    public function topDiasHistorico(int $limite = 10): array
    {
        $key = "dash_top_dias_{$limite}_{$this->codEmpresa}";
        return Cache::lembrar($key, 86400, function () use ($limite) {
            $ini = date('Y-m-d', strtotime('-730 days'));
            $fim = date('Y-m-d');
            $st  = $this->pdo->prepare("
                SELECT FIRST {$limite}
                    DATA_EMISSAO                AS DIA,
                    COUNT(*)                    AS QTD_VENDAS,
                    COALESCE(SUM(TOTAL_NOTA),0) AS TOTAL
                FROM NOTAS_CAB
                WHERE DATA_EMISSAO BETWEEN ? AND ?
                  AND DATA_CANC IS NULL
                  AND TOTAL_NOTA > 0
                  {$this->wEmp}
                GROUP BY DATA_EMISSAO
                ORDER BY TOTAL DESC
            ");
            $st->execute([$ini, $fim]);
            return $st->fetchAll();
        });
    }

    /** Faturamento mensal histórico — últimos 4 anos (usa índice em DATA_EMISSAO).
     *  TTL de 6h: meses passados não mudam; apenas o mês corrente se atualiza. */
    public function faturamentoMensalHistorico(): array
    {
        $key = 'dash_fat_mensal_hist_' . $this->codEmpresa;
        return Cache::lembrar($key, 21600, function () {
            // BETWEEN usa índice em DATA_EMISSAO; EXTRACT sem filtro = full scan
            $anoIni = (int)date('Y') - 3;
            $ini = "{$anoIni}-01-01";
            $fim = date('Y-m-d');
            $st = $this->pdo->prepare("
                SELECT
                    EXTRACT(YEAR  FROM DATA_EMISSAO) AS ANO,
                    EXTRACT(MONTH FROM DATA_EMISSAO) AS MES,
                    COUNT(*)                          AS QTD_VENDAS,
                    COALESCE(SUM(TOTAL_NOTA), 0)      AS TOTAL
                FROM NOTAS_CAB
                WHERE DATA_EMISSAO BETWEEN ? AND ?
                  AND DATA_CANC IS NULL
                  AND TOTAL_NOTA > 0
                  {$this->wEmp}
                GROUP BY
                    EXTRACT(YEAR  FROM DATA_EMISSAO),
                    EXTRACT(MONTH FROM DATA_EMISSAO)
                ORDER BY ANO, MES
            ");
            $st->execute([$ini, $fim]);
            return $st->fetchAll();
        });
    }

    /** Faturamento anual — derivado do mensal para evitar segunda query pesada.
     *  Se mensal não estiver em cache, roda query direta como fallback. */
    public function faturamentoAnualHistorico(): array
    {
        $key = 'dash_fat_anual_hist_' . $this->codEmpresa;
        return Cache::lembrar($key, 21600, function () {
            // Tenta reutilizar cache mensal (já calculado) para gerar anual em PHP
            $mensalCached = Cache::get('dash_fat_mensal_hist_' . $this->codEmpresa);
            if ($mensalCached !== null && is_array($mensalCached)) {
                $porAno = [];
                foreach ($mensalCached as $row) {
                    $ano = (int)$row['ANO'];
                    if (!isset($porAno[$ano])) {
                        $porAno[$ano] = ['ANO' => $ano, 'QTD_VENDAS' => 0, 'TOTAL' => 0.0];
                    }
                    $porAno[$ano]['QTD_VENDAS'] += (int)$row['QTD_VENDAS'];
                    $porAno[$ano]['TOTAL']       += (float)$row['TOTAL'];
                }
                $result = [];
                ksort($porAno);
                foreach ($porAno as $a) {
                    $a['TICKET_MEDIO'] = $a['QTD_VENDAS'] > 0 ? $a['TOTAL'] / $a['QTD_VENDAS'] : 0;
                    $result[] = $a;
                }
                return $result;
            }

            // Fallback: query direta (só ocorre se mensal não estiver em cache)
            $anoIni = (int)date('Y') - 3;
            $ini = "{$anoIni}-01-01";
            $fim = date('Y-m-d');
            $st = $this->pdo->prepare("
                SELECT
                    EXTRACT(YEAR FROM DATA_EMISSAO)  AS ANO,
                    COUNT(*)                          AS QTD_VENDAS,
                    COALESCE(SUM(TOTAL_NOTA), 0)      AS TOTAL,
                    COALESCE(AVG(TOTAL_NOTA), 0)      AS TICKET_MEDIO
                FROM NOTAS_CAB
                WHERE DATA_EMISSAO BETWEEN ? AND ?
                  AND DATA_CANC IS NULL
                  AND TOTAL_NOTA > 0
                  {$this->wEmp}
                GROUP BY EXTRACT(YEAR FROM DATA_EMISSAO)
                ORDER BY ANO
            ");
            $st->execute([$ini, $fim]);
            return $st->fetchAll();
        });
    }

    /** Intervalo de datas — FIRST 1 ORDER BY puro (sem filtros extras) = O(log n).
     *  DATA_CANC IS NULL e TOTAL_NOTA > 0 obrigam full scan mesmo com índice. */
    public function rangeDatasBanco(): array
    {
        $key = 'dash_range_banco_' . $this->codEmpresa;
        return Cache::lembrar($key, 3600, function () {
            try {
                // INICIO = MIN (índice ascendente, instantâneo). FIM = MAX só na janela
                // recente (últimos 120d, indexado) — NUNCA o MAX sem limite (~45s no FB 2.5
                // sem índice descendente). Ignora data futura (registro furado).
                $we = $this->codEmpresa > 0 ? "AND COD_EMPRESA = {$this->codEmpresa}" : "";
                $ini = $this->escalar("SELECT MIN(DATA_EMISSAO) FROM NOTAS_CAB WHERE DATA_EMISSAO <= CURRENT_DATE {$we}");
                $fim = $this->escalar("
                    SELECT MAX(DATA_EMISSAO) FROM NOTAS_CAB
                    WHERE DATA_EMISSAO >= DATEADD(DAY, -120, CURRENT_DATE)
                      AND DATA_EMISSAO <= CURRENT_DATE {$we}
                ");
                if ($ini instanceof \DateTime) $ini = $ini->format('Y-m-d');
                if ($fim instanceof \DateTime) $fim = $fim->format('Y-m-d');
                $ini = $ini ? substr(trim((string)$ini), 0, 10) : null;
                $fim = $fim ? substr(trim((string)$fim), 0, 10) : date('Y-m-d');
                return ['INICIO' => $ini, 'FIM' => $fim, 'TOTAL_NOTAS' => 0];
            } catch (\Throwable) {
                return ['INICIO' => null, 'FIM' => null, 'TOTAL_NOTAS' => 0];
            }
        });
    }

    // -------------------------------------------------------------------------
    // Mesas
    // -------------------------------------------------------------------------

    public function mesasAbertas(): int
    {
        $wEmpAm = $this->codEmpresa > 0 ? " AND COD_EMPRESA = {$this->codEmpresa}" : '';
        $key    = "dash_mesas_abertas_{$this->codEmpresa}";
        return (int) Cache::lembrar($key, 60, fn() => $this->escalar("
            SELECT COUNT(*)
            FROM ABERT_MESA
            WHERE STATUS = 'A'
              {$wEmpAm}
        "));
    }

    public function listaMesasAbertas(): array
    {
        $wEmpAm = $this->codEmpresa > 0 ? " AND am.COD_EMPRESA = {$this->codEmpresa}" : '';
        $key    = "dash_mesas_lista_{$this->codEmpresa}";
        return Cache::lembrar($key, 60, function () use ($wEmpAm) {
            return $this->pdo->query("
                SELECT FIRST 20
                    am.COD_ABERTURA,
                    COALESCE(TRIM(m.DESCRICAO), 'Mesa ' || am.COD_MESA) AS MESA,
                    am.HORA_ABERTURA,
                    am.DATA_ABERTURA,
                    COALESCE(am.TOTAL_ITENS, 0) AS TOTAL,
                    COALESCE(am.QTD_PESSOAS, 0) AS PESSOAS
                FROM ABERT_MESA am
                LEFT JOIN MESAS m ON m.COD_MESA    = am.COD_MESA
                                  AND m.COD_EMPRESA = am.COD_EMPRESA
                WHERE am.STATUS = 'A'
                  {$wEmpAm}
                ORDER BY am.DATA_ABERTURA, am.HORA_ABERTURA
            ")->fetchAll();
        });
    }

    // -------------------------------------------------------------------------
    // Financeiro
    // -------------------------------------------------------------------------

    public function contasReceberVencer(): float
    {
        $wEmpRp = $this->codEmpresa > 0 ? " AND COD_EMPRESA = {$this->codEmpresa}" : '';
        $key    = "dash_receber_{$this->codEmpresa}";
        // BETWEEN usa índice em VENCIMENTO; limite de 12 meses evita scan de linhas antigas/futuras
        $limFut = date('Y-m-d', strtotime('+365 days'));
        return (float) Cache::lembrar($key, 300, fn() => $this->escalar("
            SELECT COALESCE(SUM(SALDO_TITULO), 0)
            FROM RECEBER_PAGAR
            WHERE VENCIMENTO BETWEEN CURRENT_DATE AND '{$limFut}'
              AND SALDO_TITULO > 0
              {$wEmpRp}
              AND COD_TIPO_OPERACAO IN (
                  SELECT COD_TIPO_OPERACAO FROM OPERACOES WHERE ENTRADASAIDA = 'E'
              )
        "));
    }

    public function contasPagarVencer(): float
    {
        $wEmpRp = $this->codEmpresa > 0 ? " AND COD_EMPRESA = {$this->codEmpresa}" : '';
        $key    = "dash_pagar_{$this->codEmpresa}";
        $limFut = date('Y-m-d', strtotime('+365 days'));
        return (float) Cache::lembrar($key, 300, fn() => $this->escalar("
            SELECT COALESCE(SUM(SALDO_TITULO), 0)
            FROM RECEBER_PAGAR
            WHERE VENCIMENTO BETWEEN CURRENT_DATE AND '{$limFut}'
              AND SALDO_TITULO > 0
              {$wEmpRp}
              AND COD_TIPO_OPERACAO IN (
                  SELECT COD_TIPO_OPERACAO FROM OPERACOES WHERE ENTRADASAIDA = 'S'
              )
        "));
    }

    // -------------------------------------------------------------------------
    // Produtos
    // -------------------------------------------------------------------------

    public function topProdutosHoje(int $limite = 10): array
    {
        $ttl = $this->eHoje ? 60 : 1800;
        $key = "dash_top_prod_hoje_{$this->dataRef}_{$limite}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () use ($limite) {
            try {
                // Filtro em ni.DATA_EMISSAO usa índice em NOTAS_ITENS diretamente
                $st = $this->pdo->prepare("
                    SELECT FIRST {$limite}
                        MIN(ni.COD_ITEM)            AS COD_ITEM,
                        COALESCE(TRIM(ni.DESCRICAO), '(sem descrição)') AS PRODUTO,
                        SUM(ni.QTD)                 AS QTD,
                        SUM(ni.QTD * ni.PRECO_UNIT) AS TOTAL
                    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
                    WHERE ni.DATA_EMISSAO = ?
                      AND nc.DATA_CANC IS NULL
                      AND ni.DESCRICAO IS NOT NULL
                      AND TRIM(ni.DESCRICAO) <> ''
                      {$this->wEmpNc}
                    GROUP BY COALESCE(TRIM(ni.DESCRICAO), '(sem descrição)')
                    ORDER BY TOTAL DESC
                ");
                $st->execute([$this->dataRef]);
                return $st->fetchAll();
            } catch (\Throwable) { return []; }
        });
    }

    public function topProdutosSemana(int $limite = 10): array
    {
        $ini = date('Y-m-d', strtotime($this->dataRef . ' -6 days'));
        $ttl = $this->eHoje ? 60 : 1800;
        $key = "dash_top_prod_semana_{$this->dataRef}_{$limite}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () use ($limite, $ini) {
            try {
                $st = $this->pdo->prepare("
                    SELECT FIRST {$limite}
                        MIN(ni.COD_ITEM)            AS COD_ITEM,
                        COALESCE(TRIM(ni.DESCRICAO), '(sem descrição)') AS PRODUTO,
                        SUM(ni.QTD)                 AS QTD,
                        SUM(ni.QTD * ni.PRECO_UNIT) AS TOTAL
                    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
                    WHERE ni.DATA_EMISSAO BETWEEN ? AND ?
                      AND nc.DATA_CANC IS NULL
                      AND ni.DESCRICAO IS NOT NULL
                      AND TRIM(ni.DESCRICAO) <> ''
                      {$this->wEmpNc}
                    GROUP BY COALESCE(TRIM(ni.DESCRICAO), '(sem descrição)')
                    ORDER BY TOTAL DESC
                ");
                $st->execute([$ini, $this->dataRef]);
                return $st->fetchAll();
            } catch (\Throwable) { return []; }
        });
    }

    /** Top produtos — últimos 12 meses.
     *  Sem JOIN com NOTAS_CAB: usa IDX_NOTAS_ITENS_EMPRESA_DATA (COD_EMPRESA, DATA_EMISSAO)
     *  diretamente — seek exato + range scan sem lookup de tabela secundária.
     *  Notas canceladas são < 1% do volume histórico; irrelevante para ranking visual. */
    public function topProdutosHistorico(int $limite = 15): array
    {
        $ini12m = date('Y-m-d', strtotime($this->dataRef . ' -365 days'));
        $key = "dash_top_prod_hist_{$limite}_{$this->codEmpresa}";
        return Cache::lembrar($key, 21600, function () use ($limite, $ini12m) {
            try {
                $wNi = $this->codEmpresa > 0 ? " AND ni.COD_EMPRESA = {$this->codEmpresa}" : '';
                $st = $this->pdo->prepare("
                    SELECT FIRST {$limite}
                        MIN(ni.COD_ITEM)            AS COD_ITEM,
                        TRIM(ni.DESCRICAO)          AS PRODUTO,
                        SUM(ni.QTD)                 AS QTD,
                        SUM(ni.QTD * ni.PRECO_UNIT) AS TOTAL,
                        COUNT(DISTINCT ni.NR_NOTA)  AS QTD_NOTAS
                    FROM NOTAS_ITENS ni
                    WHERE ni.DATA_EMISSAO >= ?
                      {$wNi}
                    GROUP BY TRIM(ni.DESCRICAO)
                    ORDER BY TOTAL DESC
                ");
                $st->execute([$ini12m]);
                return $st->fetchAll();
            } catch (\Throwable) { return []; }
        });
    }

    // -------------------------------------------------------------------------
    // Gráficos
    // -------------------------------------------------------------------------

    /** Faturamento por dia nos últimos N dias a partir da data de referência */
    public function faturamentoDiario(int $dias = 30): array
    {
        $ini = date('Y-m-d', strtotime($this->dataRef . " -{$dias} days"));
        $ttl = $this->eHoje ? 60 : 1800;
        $key = "dash_fat_diario_{$this->dataRef}_{$dias}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () use ($ini) {
            try {
                return $this->pdo->query("
                    SELECT
                        DATA_EMISSAO                AS DIA,
                        COALESCE(SUM(TOTAL_NOTA),0) AS TOTAL
                    FROM NOTAS_CAB
                    WHERE DATA_EMISSAO BETWEEN '{$ini}' AND '{$this->dataRef}'
                      AND DATA_CANC IS NULL
                      {$this->wEmp}
                    GROUP BY DATA_EMISSAO
                    ORDER BY DATA_EMISSAO
                ")->fetchAll();
            } catch (\Throwable) { return []; }
        });
    }

    /** Faturamento por hora no dia de referência */
    public function faturamentoPorHoraHoje(): array
    {
        $ttl = $this->eHoje ? 300 : 1800;
        $key = "dash_fat_hora_{$this->dataRef}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () {
            try {
                return $this->pdo->query("
                    SELECT
                        EXTRACT(HOUR FROM DATA_HORA_VENDA) AS HORA,
                        COALESCE(SUM(TOTAL_NOTA), 0)       AS TOTAL,
                        COUNT(*)                            AS QTDE
                    FROM NOTAS_CAB
                    WHERE DATA_EMISSAO = '{$this->dataRef}'
                      AND DATA_CANC IS NULL
                      AND DATA_HORA_VENDA IS NOT NULL
                      {$this->wEmp}
                    GROUP BY EXTRACT(HOUR FROM DATA_HORA_VENDA)
                    ORDER BY HORA
                ")->fetchAll();
            } catch (\Throwable) { return []; }
        });
    }

    // -------------------------------------------------------------------------
    // Consolidado — métodos novos (ranking grupos/marcas/formas + comparativos)
    // -------------------------------------------------------------------------

    /**
     * Top N grupos (ESTRUTURA) por faturamento no intervalo — paridade lógica
     * com VendasRepository::vendasPorGrupo, mas roda no PDO da loja conectada
     * e respeita filtro de empresa + filtroVenda interno desta instância.
     *
     * Retorno: ['linhas' => [...{cod_grupo,nome,faturamento,qtd_notas,qtd_itens,
     *                             ticket_medio,participacao_pct}], 'outros'=>null|{...}].
     */
    public function vendasPorGrupoConsolidado(string $dataIni, string $dataFim, int $limite = 20): array
    {
        $limite = max(1, min(50, $limite));
        $ttl    = ($dataFim >= date('Y-m-d')) ? 60 : 1800;
        $key    = "dash_vg_cons_v_{$dataIni}_{$dataFim}_{$limite}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () use ($dataIni, $dataFim, $limite) {
            $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 nc.DATA_EMISSAO >= '{$dataIni}'
                      AND nc.DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$dataFim}' AS DATE))
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      {$this->wEmpNc}{$this->wVendaNc()}
                    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 = [];
            }

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

    /**
     * Top N marcas por faturamento no intervalo — mesma estrutura do anterior.
     */
    public function vendasPorMarcaConsolidado(string $dataIni, string $dataFim, int $limite = 20): array
    {
        $limite = max(1, min(50, $limite));
        $ttl    = ($dataFim >= date('Y-m-d')) ? 60 : 1800;
        $key    = "dash_vm_cons_v_{$dataIni}_{$dataFim}_{$limite}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () use ($dataIni, $dataFim, $limite) {
            $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 nc.DATA_EMISSAO >= '{$dataIni}'
                      AND nc.DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$dataFim}' AS DATE))
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      {$this->wEmpNc}{$this->wVendaNc()}
                    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 = [];
            }

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

    /**
     * Formas de pagamento no intervalo — espelha o relatório fiscal do ERP
     * (SEM filtroVenda, inclui crediário/cortesia/delivery). Retorna TODAS
     * as formas cadastradas que tiveram movimento no período, 1 linha por
     * forma (nome do cadastro), ordenado por valor desc. SEM agrupamento
     * "Outros" — pedido do Derlivon pra ver todas individualmente.
     *
     * Retorno: ['grupos' => [{GRUPO, VALOR, QTD}]] (chave 'grupos' mantida
     * pra compatibilidade da view/controller; cada item é uma forma única).
     */
    public function formasPagamentoIntervalo(string $dataIni, string $dataFim): array
    {
        $ttl = ($dataFim >= date('Y-m-d')) ? 60 : 1800;
        $key = "dash_formas_pgto_v2_{$dataIni}_{$dataFim}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () use ($dataIni, $dataFim) {
            try {
                $wEmp = $this->codEmpresa > 0 ? "AND nc.COD_EMPRESA = {$this->codEmpresa}" : '';
                // 1 linha por DESCRIÇÃO (upper+trim) — junta variações de capitalização
                // duplicadas no cadastro do Fire ("Pix" vs "PIX") sem perder formas reais.
                $sql = "SELECT
                            UPPER(TRIM(fp.DESCRICAO)) FORMA,
                            COUNT(*)                  QTD,
                            SUM(fn.VALOR)             VALOR
                        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 nc.DATA_EMISSAO BETWEEN ? AND ?
                          AND nc.DATA_CANC IS NULL
                          {$wEmp}
                        GROUP BY UPPER(TRIM(fp.DESCRICAO))
                        ORDER BY 3 DESC";
                $st = $this->pdo->prepare($sql);
                $st->execute([$dataIni, $dataFim]);
                $rows = $st->fetchAll(\PDO::FETCH_ASSOC);

                $itens = [];
                foreach ($rows as $r) {
                    $nome = (string)$r['FORMA'];
                    if ($nome === '') continue;
                    // Conserta encoding (Windows-1252 -> UTF-8) se o nome veio com acento quebrado
                    if (!mb_check_encoding($nome, 'UTF-8')) {
                        $nome = mb_convert_encoding($nome, 'UTF-8', 'Windows-1252');
                    }
                    // Title Case pra ficar agradável na tabela ("DINHEIRO" -> "Dinheiro").
                    $rotulo = mb_convert_case(mb_strtolower($nome, 'UTF-8'), MB_CASE_TITLE, 'UTF-8');
                    $itens[] = [
                        'GRUPO' => $rotulo,
                        'VALOR' => (float)$r['VALOR'],
                        'QTD'   => (int)$r['QTD'],
                    ];
                }
                return ['grupos' => $itens];
            } catch (\Throwable) {
                return ['grupos' => []];
            }
        });
    }

    /**
     * Versão "rica" de formasPagamentoIntervalo() que adiciona dois totais
     * pra resolver a divergência visual com o card de Faturamento.
     *
     * Retorno:
     *   [
     *     'grupos'       => [...]   // mesma lista que formasPagamentoIntervalo (todas as formas)
     *     'total_geral'  => float   // soma de TODAS as formas (espelha o relatório fiscal do ERP Fire)
     *     'total_vendas' => float|null // soma com filtroVenda aplicado (bate com faturamentoIntervalo);
     *                                  // null se a query falhou
     *   ]
     *
     * total_geral inclui crediário, cortesia, devolução com troca etc.
     * total_vendas filtra só operações de venda real (mesmo wVenda que faturamento).
     * A diferença entre os dois é exatamente o "valor fantasma" que confunde
     * o usuário comparando com o card de Faturamento ao lado.
     *
     * Mantemos o método antigo (formasPagamentoIntervalo) intacto pra não
     * quebrar nenhum consumidor existente.
     */
    public function formasPagamentoIntervaloComTotal(string $dataIni, string $dataFim): array
    {
        $ttl = ($dataFim >= date('Y-m-d')) ? 60 : 1800;
        $key = "dash_formas_pgto_comtotal_v3_{$dataIni}_{$dataFim}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () use ($dataIni, $dataFim) {
            // grupos + total_geral vêm da query "fiscal" antiga
            $base = $this->formasPagamentoIntervalo($dataIni, $dataFim);
            $grupos = $base['grupos'] ?? [];
            $totalGeral = 0.0;
            foreach ($grupos as $g) {
                $totalGeral += (float)($g['VALOR'] ?? 0);
            }

            // total_vendas: mesma query, mas com wVenda aplicado em nc.COD_TIPO_OPERACAO
            $totalVendas = null;
            try {
                $wEmp   = $this->codEmpresa > 0 ? "AND nc.COD_EMPRESA = {$this->codEmpresa}" : '';
                $wVenda = $this->wVendaNc(); // já vem com prefixo nc.; pode ser '' se loja não tem filtro
                $sql = "SELECT SUM(fn.VALOR) 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 nc.DATA_EMISSAO BETWEEN ? AND ?
                           AND nc.DATA_CANC IS NULL
                           {$wEmp}{$wVenda}";
                $st = $this->pdo->prepare($sql);
                $st->execute([$dataIni, $dataFim]);
                $v = $st->fetchColumn();
                $totalVendas = $v !== false && $v !== null ? (float)$v : 0.0;
            } catch (\Throwable) {
                // Mantém null — view mostra só total_geral
                $totalVendas = null;
            }

            // Drill-down: 1 linha por (TIPO_OPERACAO × FORMA) FORA do filtroVenda.
            // É exatamente o que compõe a "diferença" entre total_geral e total_vendas:
            // recebimento de crediário, cortesia, devolução com troca, mov interno etc.
            // Isolado em try/catch: se a query falhar, devolve [] e a view simplesmente
            // não mostra o botão de drill-down (graceful degrade).
            $detalheForaVenda = [];
            try {
                $detalheForaVenda = $this->formasPagamentoForaVenda($dataIni, $dataFim);
            } catch (\Throwable) {
                $detalheForaVenda = [];
            }

            return [
                'grupos'              => $grupos,
                'total_geral'         => $totalGeral,
                'total_vendas'        => $totalVendas,
                'detalhe_fora_venda'  => $detalheForaVenda,
            ];
        });
    }

    /**
     * Drill-down da "diferença" entre Total Geral e Total Vendas no card de
     * Formas de Pagamento. Devolve 1 linha por (TIPO_OPERACAO × FORMA),
     * pegando SÓ notas que NÃO estão na whitelist de venda real (ou seja,
     * exatamente o conjunto de operações que compõe a diferença).
     *
     * Tipicamente: RECEBIMENTO CREDIARIO, CORTESIA, DEVOLUCAO COM TROCA,
     * MOV. INTERNO, BAIXA DE CONTAS A RECEBER, etc.
     *
     * Retorno (ordenado por valor desc):
     *   [
     *     ['operacao' => 'RECEBIMENTO CREDIARIO', 'forma' => 'A VISTA DINHEIRO', 'qtd' => 5, 'valor' => 1234.56],
     *     ['operacao' => 'CORTESIA',              'forma' => 'CONSUMO FUNCIONARIO', 'qtd' => 2, 'valor' => 222.94],
     *     ...
     *   ]
     *
     * Devolve [] se:
     *   - loja não tem filtroVenda configurado (não tem "fora da venda")
     *   - query falhou por schema diferente (sem TIPO_OPERACAO, etc.)
     */
    public function formasPagamentoForaVenda(string $dataIni, string $dataFim): array
    {
        $listaCsv = $this->codigosVendaCsv();
        // Loja sem filtroVenda → não há "fora da venda" (tudo entra no Total Vendas)
        if ($listaCsv === '') return [];

        $ttl = ($dataFim >= date('Y-m-d')) ? 60 : 600;
        $key = "dash_formas_fora_venda_v2_{$dataIni}_{$dataFim}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () use ($dataIni, $dataFim, $listaCsv) {
            try {
                $wEmpNc = $this->codEmpresa > 0 ? "AND nc.COD_EMPRESA = {$this->codEmpresa}" : '';
                // PK de TIPO_OPERACAO eh (COD_TIPO_OPERACAO, COD_EMPRESA). O JOIN PRECISA
                // casar pelas DUAS colunas — sem isso, cada linha de FORMAS_NOTAS x NOTAS_CAB
                // eh amplificada 1x por empresa cadastrada com aquele codigo de operacao,
                // inflando QTD e SUM(VALOR) na mesma proporcao. Bug confirmado em 26/06/2026
                // (Achadinhos 10/20 com 3 empresas no banco -> detalhe inflado 3x).
                $sql = "SELECT
                            UPPER(TRIM(tipo.DESCRICAO)) OPERACAO,
                            UPPER(TRIM(fp.DESCRICAO))   FORMA,
                            COUNT(*)                    QTD,
                            SUM(fn.VALOR)               VALOR
                        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 tipo ON tipo.COD_TIPO_OPERACAO = nc.COD_TIPO_OPERACAO
                                              AND tipo.COD_EMPRESA       = nc.COD_EMPRESA
                        WHERE nc.DATA_EMISSAO BETWEEN ? AND ?
                          AND nc.DATA_CANC IS NULL
                          {$wEmpNc}
                          AND nc.COD_TIPO_OPERACAO NOT IN ({$listaCsv})
                        GROUP BY UPPER(TRIM(tipo.DESCRICAO)), UPPER(TRIM(fp.DESCRICAO))
                        ORDER BY 4 DESC";
                $st = $this->pdo->prepare($sql);
                $st->execute([$dataIni, $dataFim]);
                $rows = $st->fetchAll(\PDO::FETCH_ASSOC);

                $out = [];
                foreach ($rows as $r) {
                    $op = (string)($r['OPERACAO'] ?? '');
                    $fm = (string)($r['FORMA'] ?? '');
                    // Conserta Windows-1252 → UTF-8 se vier acento quebrado
                    if ($op !== '' && !mb_check_encoding($op, 'UTF-8')) {
                        $op = mb_convert_encoding($op, 'UTF-8', 'Windows-1252');
                    }
                    if ($fm !== '' && !mb_check_encoding($fm, 'UTF-8')) {
                        $fm = mb_convert_encoding($fm, 'UTF-8', 'Windows-1252');
                    }
                    $out[] = [
                        'operacao' => $op !== '' ? $op : 'OPERAÇÃO SEM DESCRIÇÃO',
                        'forma'    => $fm !== '' ? $fm : 'FORMA SEM DESCRIÇÃO',
                        'qtd'      => (int)($r['QTD'] ?? 0),
                        'valor'    => (float)($r['VALOR'] ?? 0),
                    ];
                }
                return $out;
            } catch (\Throwable) {
                return [];
            }
        });
    }

    /**
     * Faturamento + qtd vendas no mesmo intervalo deslocado -1 ano (mesma
     * "data calendário" do ano anterior). Útil pro comparativo "vs ano passado"
     * sem precisar mexer no intervalo MES/ANO inteiro.
     */
    public function faturamentoMesmaDataAnoAnterior(string $dataIni, string $dataFim): array
    {
        $iniAnt = date('Y-m-d', strtotime($dataIni . ' -1 year'));
        $fimAnt = date('Y-m-d', strtotime($dataFim . ' -1 year'));
        return [
            'data_ini'    => $iniAnt,
            'data_fim'    => $fimAnt,
            'faturamento' => (float) $this->faturamentoIntervalo($iniAnt, $fimAnt),
            'qtd_vendas'  => (int)   $this->totalVendasIntervalo($iniAnt, $fimAnt),
        ];
    }

    /**
     * Comparativo YTD (Year-To-Date) — acumulado do ano atual ate $dataFim,
     * versus acumulado do ano anterior ate a mesma data correspondente.
     *
     *   atual    = [Y-01-01, $dataFim]                (ex: 2026-01-01 -> 2026-06-24)
     *   anterior = [(Y-1)-01-01, $dataFim -1 year]    (ex: 2025-01-01 -> 2025-06-24)
     *
     * Diferente de faturamentoMesmaDataAnoAnterior (que compara o MESMO recorte
     * deslocado 1 ano). Aqui comparamos COMO O ANO VAI ate aqui vs ano passado
     * no mesmo ponto do calendario — apples-to-apples.
     */
    public function faturamentoYTD(string $dataIni, string $dataFim): array
    {
        $atualIni    = date('Y-01-01', strtotime($dataIni));
        $atualFim    = $dataFim;
        $anteriorIni = date('Y-01-01', strtotime($dataIni . ' -1 year'));
        $anteriorFim = date('Y-m-d',   strtotime($dataFim . ' -1 year'));
        return [
            'atual_ini'        => $atualIni,
            'atual_fim'        => $atualFim,
            'anterior_ini'     => $anteriorIni,
            'anterior_fim'     => $anteriorFim,
            'atual'            => (float) $this->faturamentoIntervalo($atualIni,    $atualFim),
            'anterior'         => (float) $this->faturamentoIntervalo($anteriorIni, $anteriorFim),
            'vendas_atual'     => (int)   $this->totalVendasIntervalo($atualIni,    $atualFim),
            'vendas_anterior'  => (int)   $this->totalVendasIntervalo($anteriorIni, $anteriorFim),
        ];
    }

    /**
     * Faturamento + qtd vendas no período ANTERIOR equivalente.
     *   tipo='semana' -> [ini-7d, fim-7d]
     *   tipo='mes'    -> deslocar -1 mês (strtotime '-1 month')
     *   tipo='ano'    -> deslocar -1 ano
     *
     * Reusa faturamentoIntervalo + totalVendasIntervalo já cacheados.
     */
    public function faturamentoPeriodoAnterior(string $dataIni, string $dataFim, string $tipo): array
    {
        switch ($tipo) {
            case 'semana':
                $iniAnt = date('Y-m-d', strtotime($dataIni . ' -7 days'));
                $fimAnt = date('Y-m-d', strtotime($dataFim . ' -7 days'));
                break;
            case 'mes':
                $iniAnt = date('Y-m-d', strtotime($dataIni . ' -1 month'));
                $fimAnt = date('Y-m-d', strtotime($dataFim . ' -1 month'));
                break;
            case 'ano':
                $iniAnt = date('Y-m-d', strtotime($dataIni . ' -1 year'));
                $fimAnt = date('Y-m-d', strtotime($dataFim . ' -1 year'));
                break;
            default:
                $iniAnt = $dataIni;
                $fimAnt = $dataFim;
        }
        return [
            'tipo'        => $tipo,
            'data_ini'    => $iniAnt,
            'data_fim'    => $fimAnt,
            'faturamento' => (float) $this->faturamentoIntervalo($iniAnt, $fimAnt),
            'qtd_vendas'  => (int)   $this->totalVendasIntervalo($iniAnt, $fimAnt),
        ];
    }

    // -------------------------------------------------------------------------
    // Consolidado — 4 indicadores extras por loja (dia semana / hora / top
    // produtos / top clientes). Cada método é independente, com try/catch
    // interno: se uma loja não tem coluna/schema esperado, devolve [] em vez
    // de derrubar o painel inteiro da loja.
    // -------------------------------------------------------------------------

    /**
     * Vendas por dia da semana num intervalo [ini,fim] — 7 linhas fixas
     * (Domingo→Sábado). Usa EXTRACT(WEEKDAY) do Firebird (0=domingo..6=sábado).
     * Respeita filtro de empresa + filtroVenda da instância.
     *
     * Retorno: [ {dia:0..6, label:'Dom'..'Sab', valor:float, qtd:int} ]
     */
    public function vendasPorDiaSemanaIntervalo(string $dataIni, string $dataFim): array
    {
        $ttl = ($dataFim >= date('Y-m-d')) ? 60 : 1800;
        $key = "dash_diasemana_int_v_{$dataIni}_{$dataFim}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () use ($dataIni, $dataFim) {
            $labels = ['Dom', 'Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb'];
            // Esqueleto fixo de 7 dias — garante que dia sem venda aparece zerado.
            $mapa = [];
            for ($i = 0; $i < 7; $i++) {
                $mapa[$i] = ['dia' => $i, 'label' => $labels[$i], 'valor' => 0.0, 'qtd' => 0];
            }
            try {
                $rows = $this->pdo->query("
                    SELECT
                        EXTRACT(WEEKDAY FROM DATA_EMISSAO) AS DIA_NUM,
                        COUNT(*)                            AS QTD,
                        COALESCE(SUM(TOTAL_NOTA), 0)        AS VALOR
                    FROM NOTAS_CAB
                    WHERE DATA_EMISSAO >= '{$dataIni}'
                      AND DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$dataFim}' AS DATE))
                      AND DATA_CANC IS NULL
                      AND TOTAL_NOTA > 0
                      {$this->wEmp}{$this->wVenda}
                    GROUP BY EXTRACT(WEEKDAY FROM DATA_EMISSAO)
                    ORDER BY DIA_NUM
                ")->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as $r) {
                    $idx = (int)$r['DIA_NUM'];
                    if ($idx >= 0 && $idx <= 6) {
                        $mapa[$idx]['qtd']   = (int)$r['QTD'];
                        $mapa[$idx]['valor'] = round((float)$r['VALOR'], 2);
                    }
                }
            } catch (\Throwable) {
                // Mantém o esqueleto zerado em caso de erro
            }
            return array_values($mapa);
        });
    }

    /**
     * Vendas por hora num intervalo [ini,fim] — 24 linhas fixas (0..23).
     * Tenta detectar a coluna de hora (DATA_HORA_VENDA → HORA_VENDA → DATA_HORA
     * → DT_HORA_EMISSAO), retorna esqueleto zerado se nenhuma servir.
     * Respeita filtro de empresa + filtroVenda da instância.
     *
     * Retorno: [ {hora:0..23, valor:float, qtd:int} ]
     */
    public function vendasPorHoraIntervalo(string $dataIni, string $dataFim): array
    {
        $ttl = ($dataFim >= date('Y-m-d')) ? 60 : 1800;
        $key = "dash_hora_int_v_{$dataIni}_{$dataFim}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () use ($dataIni, $dataFim) {
            // Esqueleto fixo 0..23
            $mapa = [];
            for ($h = 0; $h < 24; $h++) {
                $mapa[$h] = ['hora' => $h, 'valor' => 0.0, 'qtd' => 0];
            }

            // Auto-detecta coluna de hora — cache por instância (24h)
            $colsCandidatas = ['DATA_HORA_VENDA', 'HORA_VENDA', 'DATA_HORA', 'DT_HORA_EMISSAO'];
            $col = Cache::lembrar("dash_coluna_hora_{$this->codEmpresa}", 86400, function () use ($colsCandidatas) {
                foreach ($colsCandidatas as $c) {
                    try {
                        $this->pdo->query("SELECT FIRST 1 EXTRACT(HOUR FROM {$c}) FROM NOTAS_CAB WHERE {$c} IS NOT NULL")->fetch();
                        return $c;
                    } catch (\Throwable) {}
                }
                return '';
            }) ?? '';

            if ($col === '') {
                return array_values($mapa);
            }

            try {
                $rows = $this->pdo->query("
                    SELECT
                        EXTRACT(HOUR FROM {$col})    AS HORA,
                        COALESCE(SUM(TOTAL_NOTA), 0) AS VALOR,
                        COUNT(*)                     AS QTD
                    FROM NOTAS_CAB
                    WHERE DATA_EMISSAO >= '{$dataIni}'
                      AND DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$dataFim}' AS DATE))
                      AND DATA_CANC IS NULL
                      AND {$col} IS NOT NULL
                      AND TOTAL_NOTA > 0
                      {$this->wEmp}{$this->wVenda}
                    GROUP BY EXTRACT(HOUR FROM {$col})
                    ORDER BY HORA
                ")->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as $r) {
                    $h = (int)$r['HORA'];
                    if ($h >= 0 && $h <= 23) {
                        $mapa[$h]['valor'] = round((float)$r['VALOR'], 2);
                        $mapa[$h]['qtd']   = (int)$r['QTD'];
                    }
                }
            } catch (\Throwable) {
                // Esqueleto zerado
            }
            return array_values($mapa);
        });
    }

    /**
     * Top N produtos por faturamento num intervalo [ini,fim] — espelha a
     * mecânica do `vendasPorGrupoConsolidado` (filtro de venda + empresa +
     * cancelados), agrupando por COD_ITEM. Respeita override por cliente.
     *
     * Retorno: [ {cod_item:int, descricao:string, qtd:float, valor:float} ]
     */
    public function topProdutosIntervalo(string $dataIni, string $dataFim, int $limite = 10): array
    {
        $limite = max(1, min(50, $limite));
        $ttl    = ($dataFim >= date('Y-m-d')) ? 60 : 1800;
        $key    = "dash_top_prod_int_v_{$dataIni}_{$dataFim}_{$limite}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () use ($dataIni, $dataFim, $limite) {
            $linhas = [];
            try {
                $sql = "
                    SELECT FIRST {$limite}
                        ni.COD_ITEM                                                       AS COD_ITEM,
                        TRIM(MAX(ni.DESCRICAO))                                           AS DESCRICAO,
                        COALESCE(SUM(ni.QTD), 0)                                          AS QTD,
                        COALESCE(SUM(CAST(ni.QTD AS DOUBLE PRECISION) * ni.PRECO_UNIT), 0) AS VALOR
                    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
                    WHERE nc.DATA_EMISSAO >= '{$dataIni}'
                      AND nc.DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$dataFim}' AS DATE))
                      AND nc.DATA_CANC IS NULL
                      AND nc.TOTAL_NOTA > 0
                      {$this->wEmpNc}{$this->wVendaNc()}
                    GROUP BY ni.COD_ITEM
                    ORDER BY VALOR DESC
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as $r) {
                    $desc = (string)($r['DESCRICAO'] ?? '');
                    if ($desc !== '' && !mb_check_encoding($desc, 'UTF-8')) {
                        $desc = mb_convert_encoding($desc, 'UTF-8', 'Windows-1252');
                    }
                    $linhas[] = [
                        'cod_item'  => (int)($r['COD_ITEM'] ?? 0),
                        'descricao' => $desc,
                        'qtd'       => (float)($r['QTD'] ?? 0),
                        'valor'     => (float)($r['VALOR'] ?? 0),
                    ];
                }
            } catch (\Throwable) {
                // Silencioso — devolve [] se a tabela/coluna não existir
            }
            return $linhas;
        });
    }

    /**
     * Top N clientes por valor comprado num intervalo [ini,fim] — espelha
     * ClientesRepository::frequencia mas ordena por VALOR_TOTAL desc.
     * Filtra ECLIENTE='S' (pessoa marcada como cliente) + cancelados +
     * filtro de empresa + filtroVenda.
     *
     * Retorno: [ {cod_pessoa:int, nome:string, qtd_notas:int, valor:float} ]
     */
    public function topClientesIntervalo(string $dataIni, string $dataFim, int $limite = 10): array
    {
        $limite = max(1, min(50, $limite));
        $ttl    = ($dataFim >= date('Y-m-d')) ? 60 : 1800;
        $key    = "dash_top_cli_int_v_{$dataIni}_{$dataFim}_{$limite}_{$this->codEmpresa}";
        return Cache::lembrar($key, $ttl, function () use ($dataIni, $dataFim, $limite) {
            $linhas = [];
            try {
                // Filtros no alias "n" (NOTAS_CAB)
                $wEmpN   = $this->codEmpresa > 0 ? " AND n.COD_EMPRESA = {$this->codEmpresa}" : '';
                $wVendaN = '';
                if ($this->wVenda !== '') {
                    // " AND COD_TIPO_OPERACAO IN (...)" -> " AND n.COD_TIPO_OPERACAO IN (...)"
                    $wVendaN = str_replace(' AND COD_TIPO_OPERACAO ', ' AND n.COD_TIPO_OPERACAO ', $this->wVenda);
                }

                $sql = "
                    SELECT FIRST {$limite}
                        p.COD_PESSOA              AS COD_PESSOA,
                        TRIM(p.NOME)              AS NOME,
                        COUNT(DISTINCT n.NR_NOTA) AS QTD_NOTAS,
                        COALESCE(SUM(n.TOTAL_NOTA), 0) AS VALOR
                    FROM NOTAS_CAB n
                    JOIN PESSOAS p ON p.COD_PESSOA = n.COD_PESSOA
                    WHERE n.DATA_EMISSAO >= '{$dataIni}'
                      AND n.DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$dataFim}' AS DATE))
                      AND n.DATA_CANC IS NULL
                      AND n.TOTAL_NOTA > 0
                      AND p.ECLIENTE = 'S'
                      {$wEmpN}{$wVendaN}
                    GROUP BY p.COD_PESSOA, TRIM(p.NOME)
                    ORDER BY VALOR DESC
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as $r) {
                    $nome = (string)($r['NOME'] ?? '');
                    if ($nome !== '' && !mb_check_encoding($nome, 'UTF-8')) {
                        $nome = mb_convert_encoding($nome, 'UTF-8', 'Windows-1252');
                    }
                    $linhas[] = [
                        'cod_pessoa' => (int)($r['COD_PESSOA'] ?? 0),
                        'nome'       => $nome,
                        'qtd_notas'  => (int)($r['QTD_NOTAS'] ?? 0),
                        'valor'      => (float)($r['VALOR'] ?? 0),
                    ];
                }
            } catch (\Throwable) {
                // ECLIENTE pode não existir em todos os schemas — devolve []
            }
            return $linhas;
        });
    }

    /**
     * Versão "nc." do filtroVenda — o $this->wVenda usa colunas sem alias
     * (COD_TIPO_OPERACAO direto), mas as queries de itens precisam de
     * "nc.COD_TIPO_OPERACAO". Reescreve o prefixo só uma vez por instância.
     */
    private function wVendaNc(): string
    {
        if ($this->wVenda === '') return '';
        // " AND COD_TIPO_OPERACAO IN (...)" -> " AND nc.COD_TIPO_OPERACAO IN (...)"
        return str_replace(' AND COD_TIPO_OPERACAO ', ' AND nc.COD_TIPO_OPERACAO ', $this->wVenda);
    }

    /**
     * Extrai a lista de códigos (CSV "1,2,3") da string $this->wVenda
     * (" AND COD_TIPO_OPERACAO IN (1,2,3)"). Usado pelo drill-down de
     * formasPagamentoForaVenda() pra montar a NEGAÇÃO do filtroVenda
     * ("NOT IN (...)").
     *
     * Retorna '' se a loja não tem filtro de venda (tudo é venda → não
     * existe "fora da venda").
     */
    private function codigosVendaCsv(): string
    {
        if ($this->wVenda === '') return '';
        // " AND COD_TIPO_OPERACAO IN (1,2,3)" → "1,2,3"
        if (preg_match('/IN\s*\(([^)]+)\)/i', $this->wVenda, $m)) {
            return trim($m[1]);
        }
        return '';
    }

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

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