<?php

namespace App\Repositories;

use App\Core\Cache;

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

    private function ttl(string $dataFim): int
    {
        $hoje = date('Y-m-d');
        if ($dataFim >= $hoje)                                      return 300;   // hoje: 5min
        if ($dataFim >= date('Y-m-d', strtotime('-3 days')))       return 900;   // últimos 3 dias: 15min
        return 86400;                                                              // histórico: 24h (não muda)
    }

    private function codEmpresa(): ?int
    {
        try { return \App\Core\Lojas::codEmpresaAtiva() ?: null; } catch (\Throwable) { return null; }
    }

    // =========================================================================
    // RELATÓRIO DE COMPRAS (configurável) — espelho do relatório de vendas
    // =========================================================================

    public function relatorioComprasLookups(): array
    {
        $emp = $this->codEmpresa();
        return Cache::lembrar('rel_compras_lookups_' . ($emp ?: '0'), 1800, function () use ($emp) {
            $try = function ($sql) { try { return $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC); } catch (\Throwable) { return []; } };
            $utf = function ($s) { return mb_convert_encoding((string)$s, 'UTF-8', 'Windows-1252'); };
            $out = [
                'naturezas' => $try("SELECT DISTINCT t.COD_NATUREZA COD, TRIM(t.DESCRICAO) NOME FROM TIPO_OPERACAO t WHERE t.TIPO_NATUREZA='C' " . ($emp ? "AND t.COD_EMPRESA={$emp}" : '') . " ORDER BY 2"),
                'condicoes' => $try("SELECT COD_COND_PGTO COD, TRIM(DESCRICAO) NOME FROM CONDICAO_PGTO ORDER BY 2"),
                '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"),
                'usuarios'  => $try("SELECT TRIM(COD_USUARIO) COD, TRIM(COALESCE(NOME,COD_USUARIO)) NOME FROM USUARIOS " . ($emp ? "WHERE COD_EMPRESA={$emp}" : '') . " ORDER BY 2"),
            ];
            foreach (['naturezas','condicoes','usuarios'] as $k) { foreach ($out[$k] as &$r) { if (isset($r['NOME'])) $r['NOME'] = $utf($r['NOME']); } unset($r); }
            return $out;
        });
    }

    public function relatorioCompras(array $f): array
    {
        $emp = $this->codEmpresa();
        $ck  = 'relc_' . md5(json_encode($f)) . '_' . ($emp ?: '0');
        $cached = Cache::get($ck);
        if ($cached !== null) return $cached;

        $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;

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

        // 1) Resolve as operações de COMPRA (lista pequena) → filtro INDEXADO por COD_TIPO_OPERACAO.
        //    Evita varrer o mês inteiro 5-6x juntando TIPO_OPERACAO.
        $condOp = ["t.TIPO_NATUREZA = 'C'"];
        if ($emp) $condOp[] = "t.COD_EMPRESA = {$emp}";
        if (empty($f['inc_transferencias'])) $condOp[] = "(t.ETRANSF='N' OR t.ETRANSF IS NULL)";
        if (empty($f['inc_devolucoes']))     $condOp[] = "(t.DEVOLUCAO='N' OR t.DEVOLUCAO IS NULL)";
        if (empty($f['inc_mov_interna']))    $condOp[] = "(t.MOV_INTERNA='N' OR t.MOV_INTERNA IS NULL)";
        $nats = array_filter(array_map('intval', (array)($f['naturezas'] ?? [])));
        if ($nats) $condOp[] = "t.COD_NATUREZA IN (" . implode(',', $nats) . ")";
        $ops = $q("SELECT COD_TIPO_OPERACAO COD, COD_NATUREZA NAT, TRIM(DESCRICAO) DESC_ FROM TIPO_OPERACAO t WHERE " . implode(' AND ', $condOp));
        $opMap = []; $codes = [];
        foreach ($ops as $o) { $c = (int)$o['COD']; $codes[] = $c; $opMap[$c] = ['nat'=>(int)$o['NAT'], 'desc'=>$utf($o['DESC_'])]; }
        if (!$codes) {
            $out = ['resumo'=>['QTD'=>0,'FORNECEDORES'=>0,'TOTAL'=>0,'PRODUTOS'=>0,'DESCONTOS'=>0,'FRETE'=>0,'ICMS'=>0,'IPI'=>0,'ST'=>0],
                'por_fornecedor'=>[],'por_natureza'=>[],'por_serie'=>[],'notas'=>[],'produtos'=>[],'capado'=>false,'limite'=>0,'periodo'=>['ini'=>$ini,'fim'=>$fim]];
            Cache::set($ck, $out, 120); return $out;
        }
        $inOps = implode(',', $codes);

        // 2) WHERE só sobre NOTAS_CAB (sem join TIPO_OPERACAO) — data + operação indexadas.
        $w = ["n.DATA_EMISSAO >= '{$ini}' AND n.DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$fim}' AS DATE))",
              "n.COD_TIPO_OPERACAO IN ({$inOps})"];
        if ($emp) $w[] = "n.COD_EMPRESA = {$emp}";
        $canc = $f['canceladas'] ?? 'excluir';
        if ($canc === 'somente')     $w[] = "(n.COD_SITUACAO = 3 OR n.DATA_CANC IS NOT NULL)";
        elseif ($canc !== 'incluir') $w[] = "(n.COD_SITUACAO IS NULL OR n.COD_SITUACAO <> 3) AND n.DATA_CANC IS NULL";
        if (!empty($f['nfe'])) $w[] = "n.NFE = 'S'";
        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[] = "n.NR_NOTA BETWEEN {$a} AND {$b}";
        }
        if ($I($f['forn_ini'] ?? null) !== null || $I($f['forn_fim'] ?? null) !== null) {
            $a = $I($f['forn_ini'] ?? null) ?? 0; $b = $I($f['forn_fim'] ?? null) ?? 2147483647; $w[] = "n.COD_PESSOA BETWEEN {$a} AND {$b}";
        }
        if (!empty($f['serie']))             $w[] = "TRIM(n.SERIE) = " . $S(trim($f['serie']));
        if ($I($f['cond'] ?? null) !== null) $w[] = "n.COD_COND_PGTO = " . $I($f['cond']);
        if (!empty($f['usuario']))           $w[] = "UPPER(TRIM(n.USUARIO)) = UPPER(" . $S(trim($f['usuario'])) . ")";
        $WHERE = implode("\n  AND ", $w);

        // 3) UMA varredura dos cabeçalhos (+ nome do fornecedor). Agrega o resto em PHP.
        $rows = $q("SELECT FIRST 20000 n.NR_NOTA, TRIM(n.SERIE) SERIE, n.DATA_EMISSAO DATA_REF, n.COD_PESSOA,
                n.COD_TIPO_OPERACAO, TRIM(COALESCE(p.NOME,'(sem fornecedor)')) FORNECEDOR,
                COALESCE(n.TOTAL_PRODUTOS,0) PRODUTOS, COALESCE(n.DESCONTO,0) DESCONTO,
                COALESCE(COALESCE(n.FRETE_DENTRO,0)+COALESCE(n.FRETE_FORA,0),0) FRETE,
                COALESCE(n.VLR_ICMS,0) ICMS, COALESCE(n.IPI,0) IPI, COALESCE(n.ICMS_SUBST_TRIB,0) ST,
                COALESCE(n.TOTAL_NOTA,0) TOTAL_NOTA, n.COD_SITUACAO
            FROM NOTAS_CAB n LEFT JOIN PESSOAS p ON p.COD_PESSOA = n.COD_PESSOA
            WHERE {$WHERE}");

        $resumo = ['QTD'=>0,'FORNECEDORES'=>0,'TOTAL'=>0,'PRODUTOS'=>0,'DESCONTOS'=>0,'FRETE'=>0,'ICMS'=>0,'IPI'=>0,'ST'=>0];
        $forn = []; $nat = []; $ser = []; $fset = [];
        foreach ($rows as &$r) {
            $r['FORNECEDOR'] = $utf($r['FORNECEDOR']);
            if (is_object($r['DATA_REF'])) $r['DATA_REF'] = $r['DATA_REF']->format('Y-m-d');
            $tot = (float)$r['TOTAL_NOTA'];
            $resumo['QTD']++; $resumo['TOTAL']+=$tot; $resumo['PRODUTOS']+=(float)$r['PRODUTOS']; $resumo['DESCONTOS']+=(float)$r['DESCONTO'];
            $resumo['FRETE']+=(float)$r['FRETE']; $resumo['ICMS']+=(float)$r['ICMS']; $resumo['IPI']+=(float)$r['IPI']; $resumo['ST']+=(float)$r['ST'];
            $fset[$r['COD_PESSOA']] = 1;
            $fn = $r['FORNECEDOR']; if (!isset($forn[$fn])) $forn[$fn]=['NOME'=>$fn,'QTD'=>0,'TOTAL'=>0]; $forn[$fn]['QTD']++; $forn[$fn]['TOTAL']+=$tot;
            $nn = $opMap[(int)$r['COD_TIPO_OPERACAO']]['desc'] ?? '(?)'; if (!isset($nat[$nn])) $nat[$nn]=['NOME'=>$nn,'QTD'=>0,'TOTAL'=>0]; $nat[$nn]['QTD']++; $nat[$nn]['TOTAL']+=$tot;
            $ss = $r['SERIE']; if (!isset($ser[$ss])) $ser[$ss]=['NOME'=>$ss,'QTD'=>0,'TOTAL'=>0]; $ser[$ss]['QTD']++; $ser[$ss]['TOTAL']+=$tot;
        }
        unset($r);
        $resumo['FORNECEDORES'] = count($fset);
        $sortT = fn($a,$b) => $b['TOTAL'] <=> $a['TOTAL'];
        $forn = array_values($forn); usort($forn, $sortT); $forn = array_slice($forn, 0, 100);
        $nat = array_values($nat); usort($nat, $sortT);
        $ser = array_values($ser); usort($ser, $sortT);

        $lim = max(50, min(20000, (int)($f['limite'] ?? 1000)));
        $ordKey = $f['ordenar'] ?? 'data';
        usort($rows, function ($a, $b) use ($ordKey) {
            if ($ordKey==='valor')      return (float)$b['TOTAL_NOTA'] <=> (float)$a['TOTAL_NOTA'];
            if ($ordKey==='nota')       return (int)$a['NR_NOTA'] <=> (int)$b['NR_NOTA'];
            if ($ordKey==='fornecedor') return strcmp($a['FORNECEDOR'], $b['FORNECEDOR']);
            return strcmp((string)$a['DATA_REF'], (string)$b['DATA_REF']);
        });
        $notas = array_slice($rows, 0, $lim);
        foreach ($notas as &$r) { $r['OPERACAO'] = $opMap[(int)$r['COD_TIPO_OPERACAO']]['desc'] ?? ''; } unset($r);

        $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
                FROM NOTAS_ITENS ni
                WHERE ni.DATA_EMISSAO >= '{$ini}' AND ni.DATA_EMISSAO < DATEADD(DAY, 1, CAST('{$fim}' AS DATE))
                  AND ni.COD_TIPO_OPERACAO IN ({$inOps})" . ($emp ? " AND ni.COD_EMPRESA = {$emp}" : "") . "
                GROUP BY ni.COD_ITEM ORDER BY 4 DESC");
            foreach ($produtos as &$r) { $r['DESCRICAO'] = $utf($r['DESCRICAO']); } unset($r);
        }

        $out = [
            'resumo'         => $resumo,
            'por_fornecedor' => $forn,
            'por_natureza'   => $nat,
            'por_serie'      => $ser,
            'notas'          => $notas,
            'produtos'       => $produtos,
            'capado'         => count($rows) >= 20000,
            'limite'         => $lim,
            'periodo'        => ['ini'=>$ini, 'fim'=>$fim],
        ];
        Cache::set($ck, $out, 120);
        return $out;
    }

    // =========================================================================
    // CUSTO MÉDIO DE COMPRA — controle de "comprar caro"
    // Custo médio por peça = VALOR da nota COM imposto (mercadorias + IPI + ST +
    // frete + seguro + outras desp.) ÷ total de PEÇAS (Σ quantidade dos itens).
    // Régua (alvo) padrão R$6: 🟢 dentro / 🔴 acima. Notas sem valor lançado são
    // sinalizadas (depósito ainda não lançou o custo).
    // Valor é calculado pelo ITEM (PRECO_UNIT + impostos por linha) com fallback
    // no cabeçalho (TOTAL_NOTA) — na prática a maioria das notas vem com valor só
    // no item ou ainda zerada.
    // =========================================================================
    public function custoMedioCompra(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-01');
        $fim = $D($f['data_fim'] ?? null) ?: date('Y-m-d');
        if ($fim < $ini) $fim = $ini;

        $alvo    = (float)($f['alvo'] ?? 6); if ($alvo <= 0) $alvo = 6;
        $apenas  = (string)($f['apenas'] ?? '');        // '' | 'acima' | 'sem_valor'
        $tipoF   = (string)($f['tipo'] ?? '');          // '' (tudo) | 'nf' | 'manual'
        $ordenar = (string)($f['ordenar'] ?? 'custo');  // custo | data | valor | fornecedor
        $busca   = trim((string)($f['busca'] ?? ''));
        $lim     = max(20, min(5000, (int)($f['limite'] ?? 1000)));
        $emp     = $this->codEmpresa();

        // Parte pesada (banco) só depende de período+empresa -> cache 5min.
        // Mudar régua/ordem/busca NÃO re-consulta o banco.
        $base = Cache::lembrar('compras_customedio_' . md5(json_encode([$ini, $fim, $emp])), 300, function () use ($ini, $fim, $emp) {
            $utf = fn($s) => ($s !== null && $s !== '' && !mb_check_encoding((string)$s, 'UTF-8'))
                ? mb_convert_encoding((string)$s, 'UTF-8', 'Windows-1252') : (string)$s;
            $ops = "(SELECT t.COD_TIPO_OPERACAO FROM TIPO_OPERACAO t WHERE t.TIPO_NATUREZA='C')";
            $eN  = $emp ? " AND n.COD_EMPRESA = {$emp}"  : '';
            $eI  = $emp ? " AND ni.COD_EMPRESA = {$emp}" : '';
            $jan = "DATEADD(DAY,1,CAST('{$fim}' AS DATE))";

            // Mapa das operações de natureza C (descrição + série) p/ classificar a nota
            // em "compra de NF" (fornecedor real) x "entrada manual" (F5, sem fornecedor).
            $opMap = [];
            foreach ($this->pdo->query("SELECT t.COD_TIPO_OPERACAO OP, TRIM(t.DESCRICAO) OPER, TRIM(COALESCE(t.SERIE,'')) SERIE
                                        FROM TIPO_OPERACAO t WHERE t.TIPO_NATUREZA='C'")->fetchAll(\PDO::FETCH_ASSOC) as $o) {
                $opMap[(int)$o['OP']] = ['oper' => $utf($o['OPER']), 'serie' => $o['SERIE']];
            }

            $heads = $this->pdo->query(
                "SELECT FIRST 20000 n.NR_NOTA, n.COD_TIPO_OPERACAO OP, n.DATA_EMISSAO DT,
                        TRIM(COALESCE(p.NOME,'(sem fornecedor)')) FORN, COALESCE(n.TOTAL_NOTA,0) TOTAL_NOTA
                 FROM NOTAS_CAB n LEFT JOIN PESSOAS p ON p.COD_PESSOA = n.COD_PESSOA
                 WHERE n.DATA_EMISSAO >= '{$ini}' AND n.DATA_EMISSAO < {$jan}
                   AND n.DATA_CANC IS NULL AND n.COD_TIPO_OPERACAO IN {$ops}{$eN}"
            )->fetchAll(\PDO::FETCH_ASSOC);

            $itens = $this->pdo->query(
                "SELECT ni.NR_NOTA, ni.COD_TIPO_OPERACAO OP, ni.COD_ITEM,
                        TRIM(MAX(ni.DESCRICAO)) DESCRICAO,
                        COALESCE(SUM(ni.QTD),0) QTD,
                        COALESCE(SUM(ni.QTD*ni.PRECO_UNIT),0) MERC,
                        COALESCE(SUM(COALESCE(ni.VLR_IPI,0)+COALESCE(ni.VALOR_ICMS_SUBTRIB,0)
                                    +COALESCE(ni.VALOR_FRETE,0)+COALESCE(ni.VALOR_SEGURO,0)
                                    +COALESCE(ni.VALOR_OUTRAS_DESP,0)),0) IMP
                 FROM NOTAS_ITENS ni
                 WHERE ni.DATA_EMISSAO >= '{$ini}' AND ni.DATA_EMISSAO < {$jan}
                   AND ni.COD_TIPO_OPERACAO IN {$ops}{$eI}
                 GROUP BY ni.NR_NOTA, ni.COD_TIPO_OPERACAO, ni.COD_ITEM"
            )->fetchAll(\PDO::FETCH_ASSOC);

            $byNota = [];
            foreach ($itens as $it) {
                $k = $it['NR_NOTA'] . '-' . $it['OP'];
                if (!isset($byNota[$k])) $byNota[$k] = ['pecas' => 0.0, 'merc' => 0.0, 'imp' => 0.0, 'itens' => []];
                $qtd = (float)$it['QTD']; $merc = (float)$it['MERC']; $imp = (float)$it['IMP'];
                $byNota[$k]['pecas'] += $qtd; $byNota[$k]['merc'] += $merc; $byNota[$k]['imp'] += $imp;
                $tot = $merc + $imp;
                $byNota[$k]['itens'][] = [
                    'cod'  => (int)$it['COD_ITEM'], 'desc' => $utf($it['DESCRICAO']),
                    'qtd'  => $qtd, 'total' => $tot, 'custo_un' => $qtd > 0 ? $tot / $qtd : 0.0,
                ];
            }

            $notas = [];
            foreach ($heads as $h) {
                $k = $h['NR_NOTA'] . '-' . $h['OP'];
                $g = $byNota[$k] ?? null;
                $pecas = $g ? $g['pecas'] : 0.0;
                if ($pecas <= 0) continue; // nota sem itens com quantidade (serviço etc.)
                $valor = $g ? ($g['merc'] + $g['imp']) : 0.0;
                if ($valor <= 0) $valor = (float)$h['TOTAL_NOTA']; // fallback: valor do cabeçalho
                $dt  = is_object($h['DT']) ? $h['DT']->format('Y-m-d') : substr((string)$h['DT'], 0, 10);
                $its = $g ? $g['itens'] : [];
                usort($its, fn($a, $b) => $b['custo_un'] <=> $a['custo_un']); // itens caros primeiro
                $op    = (int)$h['OP'];
                $oper  = $opMap[$op]['oper']  ?? '';
                $serie = $opMap[$op]['serie'] ?? '';
                // "Entrada manual" = operação cuja descrição contém MANUAL (F5) ou série ESM (sem fornecedor real).
                $tipo  = (stripos($oper, 'MANUAL') !== false || strcasecmp($serie, 'ESM') === 0) ? 'manual' : 'nf';
                $notas[] = [
                    'nr_nota'     => (int)$h['NR_NOTA'], 'data' => $dt, 'fornecedor' => $utf($h['FORN']),
                    'operacao'    => $oper, 'serie' => $serie, 'tipo' => $tipo,
                    'pecas'       => $pecas, 'valor' => $valor,
                    'custo_medio' => $valor > 0 ? $valor / $pecas : 0.0,
                    'sem_valor'   => $valor <= 0,
                    'itens'       => array_slice($its, 0, 30),
                ];
            }
            return $notas;
        });

        // Pós-processamento em PHP (não re-consulta o banco).
        // Escopo (filtro de tipo NF x entrada manual) — KPIs e lista respeitam o mesmo escopo.
        $escopo = ($tipoF === 'nf' || $tipoF === 'manual')
            ? array_values(array_filter($base, fn($n) => $n['tipo'] === $tipoF))
            : $base;

        $notas = $escopo;
        if ($busca !== '') {
            $bu = mb_strtoupper($busca, 'UTF-8');
            $notas = array_values(array_filter($notas, function ($n) use ($bu) {
                if (mb_stripos((string)$n['fornecedor'], $bu) !== false) return true;
                if (mb_stripos((string)$n['operacao'], $bu) !== false) return true;
                if (stripos((string)$n['nr_nota'], $bu) !== false) return true;
                foreach ($n['itens'] as $it) if (mb_stripos((string)$it['desc'], $bu) !== false) return true;
                return false;
            }));
        }
        foreach ($notas as &$n) {
            $n['acima'] = (!$n['sem_valor'] && $n['custo_medio'] > $alvo);
            $n['faixa'] = $n['sem_valor'] ? 'sem' : ($n['acima'] ? 'acima' : 'ok');
        }
        unset($n);
        if ($apenas === 'acima')          $notas = array_values(array_filter($notas, fn($n) => $n['acima']));
        elseif ($apenas === 'sem_valor')  $notas = array_values(array_filter($notas, fn($n) => $n['sem_valor']));

        usort($notas, function ($a, $b) use ($ordenar) {
            if ($ordenar === 'data')       return strcmp((string)$b['data'], (string)$a['data']);
            if ($ordenar === 'valor')      return $b['valor'] <=> $a['valor'];
            if ($ordenar === 'fornecedor') return strcmp((string)$a['fornecedor'], (string)$b['fornecedor']);
            return $b['custo_medio'] <=> $a['custo_medio']; // custo (padrão): mais caro primeiro
        });

        $comValor = array_filter($escopo, fn($n) => !$n['sem_valor']);
        $totP = array_sum(array_map(fn($n) => $n['pecas'], $comValor));
        $totV = array_sum(array_map(fn($n) => $n['valor'], $comValor));
        $resumo = [
            'NOTAS'             => count($escopo),
            'COM_VALOR'         => count($comValor),
            'SEM_VALOR'         => count($escopo) - count($comValor),
            'ACIMA'             => count(array_filter($escopo, fn($n) => !$n['sem_valor'] && $n['custo_medio'] > $alvo)),
            'CUSTO_MEDIO_GERAL' => $totP > 0 ? $totV / $totP : 0.0,
            'PECAS'             => $totP,
            'VALOR'             => $totV,
            'ALVO'              => $alvo,
            // contagens por tipo (sempre sobre o período inteiro, p/ mostrar a divisão)
            'QTD_NF'            => count(array_filter($base, fn($n) => $n['tipo'] === 'nf')),
            'QTD_MANUAL'        => count(array_filter($base, fn($n) => $n['tipo'] === 'manual')),
        ];

        return [
            'resumo'  => $resumo,
            'notas'   => array_slice($notas, 0, $lim),
            'alvo'    => $alvo,
            'apenas'  => $apenas,
            'tipo'    => $tipoF,
            'ordenar' => $ordenar,
            'busca'   => $busca,
            'dataIni' => $ini,
            'dataFim' => $fim,
            'capado'  => count($notas) > $lim,
        ];
    }

    public function kpis(string $dataIni, string $dataFim): array
    {
        $key = "compras_kpis_{$dataIni}_{$dataFim}";
        return Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            $sql = "SELECT
                COUNT(DISTINCT n.NR_NOTA) QTD_NOTAS,
                COUNT(DISTINCT n.COD_PESSOA) QTD_FORNECEDORES,
                SUM(n.TOTAL_NOTA) TOTAL_COMPRAS,
                SUM(n.TOTAL_PRODUTOS) TOTAL_PRODUTOS,
                SUM(n.DESCONTO) TOTAL_DESCONTO
            FROM NOTAS_CAB n
            WHERE n.COD_TIPO_OPERACAO IN (SELECT t.COD_TIPO_OPERACAO FROM TIPO_OPERACAO t WHERE t.TIPO_NATUREZA = 'C')
              AND n.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
              AND (n.COD_SITUACAO IS NULL OR n.COD_SITUACAO <> 3)";
            $st = $this->pdo->prepare($sql);
            $st->execute([$dataIni, $dataFim]);
            return $st->fetch(\PDO::FETCH_ASSOC) ?: [];
        });
    }

    /**
     * KPIs comparativos: atual × mês anterior × mesmo período ano anterior.
     * Reusa kpis() — três chamadas, cada uma já cacheada individualmente.
     *
     * Janela do mês anterior:  desloca -1 mês (mesma duração).
     * Janela do ano anterior:  desloca -12 meses (mesma duração).
     *
     * Depto de Engenharia e Automação - Master Automação - Autor: Derlivon Silva
     */
    public function kpisComparativo(string $dataIni, string $dataFim): array
    {
        try {
            $d1 = new \DateTime($dataIni);
            $d2 = new \DateTime($dataFim);

            // Mês anterior (mesmo intervalo, deslocado -1 mês)
            $mesAntIni = (clone $d1)->modify('-1 month')->format('Y-m-d');
            $mesAntFim = (clone $d2)->modify('-1 month')->format('Y-m-d');

            // Ano anterior (mesmo intervalo, deslocado -12 meses)
            $anoAntIni = (clone $d1)->modify('-12 months')->format('Y-m-d');
            $anoAntFim = (clone $d2)->modify('-12 months')->format('Y-m-d');

            $atual    = $this->kpis($dataIni, $dataFim);
            $mesAnt   = $this->kpis($mesAntIni, $mesAntFim);
            $anoAnt   = $this->kpis($anoAntIni, $anoAntFim);

            return [
                'atual'     => $atual,
                'mes_ant'   => $mesAnt,
                'ano_ant'   => $anoAnt,
                'mes_ant_ini' => $mesAntIni, 'mes_ant_fim' => $mesAntFim,
                'ano_ant_ini' => $anoAntIni, 'ano_ant_fim' => $anoAntFim,
            ];
        } catch (\Throwable) { return []; }
    }

    /**
     * Soma das DEVOLUÇÕES de compra no período (TIPO_OPERACAO.DEVOLUCAO='S').
     * NOTA importante: o kpis() acima NÃO exclui devoluções (TOTAL_COMPRAS
     * inclui as entradas marcadas como devolução). Esta métrica serve só
     * pra dar visibilidade do volume devolvido — se o cliente quiser
     * excluir, ajustar filtro do kpis() depois (decisão de negócio).
     *
     * Depto de Engenharia e Automação - Master Automação - Autor: Derlivon Silva
     */
    public function devolucoesCompras(string $dataIni, string $dataFim): array
    {
        $key = "compras_devolucoes_{$dataIni}_{$dataFim}";
        return Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            try {
                $sql = "SELECT
                    COUNT(DISTINCT n.NR_NOTA) QTD_NOTAS,
                    COALESCE(SUM(n.TOTAL_NOTA),0) TOTAL_DEVOLUCAO
                FROM NOTAS_CAB n
                JOIN TIPO_OPERACAO t ON t.COD_TIPO_OPERACAO = n.COD_TIPO_OPERACAO
                                   AND t.TIPO_NATUREZA = 'C'
                                   AND UPPER(TRIM(t.DEVOLUCAO)) = 'S'
                WHERE n.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
                  AND (n.COD_SITUACAO IS NULL OR n.COD_SITUACAO <> 3)";
                $st = $this->pdo->prepare($sql);
                $st->execute([$dataIni, $dataFim]);
                $r = $st->fetch(\PDO::FETCH_ASSOC) ?: [];
                return [
                    'QTD_NOTAS'       => (int)($r['QTD_NOTAS'] ?? 0),
                    'TOTAL_DEVOLUCAO' => (float)($r['TOTAL_DEVOLUCAO'] ?? 0),
                ];
            } catch (\Throwable) {
                return ['QTD_NOTAS' => 0, 'TOTAL_DEVOLUCAO' => 0.0];
            }
        });
    }

    /**
     * Soma de ICMS + IPI + ST + FRETE no período de compras.
     * Query enxuta (uma única passada por NOTAS_CAB) — NÃO usa o
     * relatorioCompras() que é caro (filtros complexos, agregações por nota).
     *
     * Depto de Engenharia e Automação - Master Automação - Autor: Derlivon Silva
     */
    public function kpisImpostos(string $dataIni, string $dataFim): array
    {
        $key = "compras_impostos_{$dataIni}_{$dataFim}";
        return Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            try {
                $sql = "SELECT
                    COALESCE(SUM(n.VLR_ICMS),0)         ICMS,
                    COALESCE(SUM(n.IPI),0)              IPI,
                    COALESCE(SUM(n.ICMS_SUBST_TRIB),0)  ST,
                    COALESCE(SUM(COALESCE(n.FRETE_DENTRO,0)+COALESCE(n.FRETE_FORA,0)),0) FRETE,
                    COALESCE(SUM(n.TOTAL_NOTA),0)       TOTAL_NOTA
                FROM NOTAS_CAB n
                WHERE n.COD_TIPO_OPERACAO IN (SELECT t.COD_TIPO_OPERACAO FROM TIPO_OPERACAO t WHERE t.TIPO_NATUREZA = 'C')
                  AND n.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
                  AND (n.COD_SITUACAO IS NULL OR n.COD_SITUACAO <> 3)";
                $st = $this->pdo->prepare($sql);
                $st->execute([$dataIni, $dataFim]);
                $r = $st->fetch(\PDO::FETCH_ASSOC) ?: [];
                $icms  = (float)($r['ICMS']  ?? 0);
                $ipi   = (float)($r['IPI']   ?? 0);
                $st_   = (float)($r['ST']    ?? 0);
                $frete = (float)($r['FRETE'] ?? 0);
                return [
                    'ICMS'       => $icms,
                    'IPI'        => $ipi,
                    'ST'         => $st_,
                    'FRETE'      => $frete,
                    'TOTAL'      => $icms + $ipi + $st_ + $frete,
                    'TOTAL_NOTA' => (float)($r['TOTAL_NOTA'] ?? 0),
                ];
            } catch (\Throwable) {
                return ['ICMS'=>0,'IPI'=>0,'ST'=>0,'FRETE'=>0,'TOTAL'=>0,'TOTAL_NOTA'=>0];
            }
        });
    }

    public function notasEntrada(string $dataIni, string $dataFim, int $limite = 50): array
    {
        $key = "compras_notas_{$dataIni}_{$dataFim}_{$limite}";
        return Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $limite) {
            // Firebird exige FIRST antes de DISTINCT: SELECT FIRST n DISTINCT ...
            // O DISTINCT evita duplicação quando TIPO_OPERACAO tem múltiplas linhas
            // com o mesmo COD_TIPO_OPERACAO e TIPO_NATUREZA='C'.
            $sql = "SELECT FIRST {$limite} DISTINCT
                n.NR_NOTA, n.SERIE, n.DATA_EMISSAO,
                n.TOTAL_NOTA, n.TOTAL_PRODUTOS, n.DESCONTO,
                n.COD_SITUACAO,
                p.NOME FORNECEDOR,
                t.DESCRICAO TIPO_OP
            FROM NOTAS_CAB n
            JOIN TIPO_OPERACAO t ON t.COD_TIPO_OPERACAO = n.COD_TIPO_OPERACAO AND t.TIPO_NATUREZA = 'C'
            LEFT JOIN PESSOAS p ON n.COD_PESSOA = p.COD_PESSOA
            WHERE n.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
              AND (n.COD_SITUACAO IS NULL OR n.COD_SITUACAO <> 3)
            ORDER BY n.DATA_EMISSAO DESC, n.NR_NOTA DESC";
            $st = $this->pdo->prepare($sql);
            $st->execute([$dataIni, $dataFim]);
            return $st->fetchAll(\PDO::FETCH_ASSOC);
        });
    }

    public function porFornecedor(string $dataIni, string $dataFim): array
    {
        $key = "compras_forn_{$dataIni}_{$dataFim}";
        return Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            $sql = "SELECT
                p.NOME FORNECEDOR,
                COUNT(DISTINCT n.NR_NOTA) QTD_NOTAS,
                SUM(n.TOTAL_NOTA) TOTAL_COMPRAS,
                MIN(n.DATA_EMISSAO) PRIMEIRA_COMPRA,
                MAX(n.DATA_EMISSAO) ULTIMA_COMPRA
            FROM NOTAS_CAB n
            LEFT JOIN PESSOAS p ON n.COD_PESSOA = p.COD_PESSOA
            WHERE n.COD_TIPO_OPERACAO IN (SELECT t.COD_TIPO_OPERACAO FROM TIPO_OPERACAO t WHERE t.TIPO_NATUREZA = 'C')
              AND n.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
              AND (n.COD_SITUACAO IS NULL OR n.COD_SITUACAO <> 3)
            GROUP BY p.NOME
            ORDER BY SUM(n.TOTAL_NOTA) DESC";
            $st = $this->pdo->prepare($sql);
            $st->execute([$dataIni, $dataFim]);
            return $st->fetchAll(\PDO::FETCH_ASSOC);
        });
    }

    public function itensMaisComprados(string $dataIni, string $dataFim, int $limite = 50): array
    {
        $key = "compras_itens_{$dataIni}_{$dataFim}_{$limite}";
        return Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $limite) {
            $sql = "SELECT FIRST {$limite}
                ni.COD_ITEM,
                i.DESCRICAO,
                SUM(ni.QTD) QTD_TOTAL,
                SUM(ni.QTD * ni.PRECO_UNIT) VALOR_TOTAL,
                AVG(ni.PRECO_UNIT) PRECO_MEDIO,
                COUNT(DISTINCT ni.NR_NOTA) QTD_NOTAS
            FROM NOTAS_ITENS ni
            JOIN TIPO_OPERACAO t ON ni.COD_TIPO_OPERACAO = t.COD_TIPO_OPERACAO
            JOIN ITENS i ON ni.COD_ITEM = i.COD_ITEM
            WHERE t.TIPO_NATUREZA = 'C'
              AND ni.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
            GROUP BY ni.COD_ITEM, i.DESCRICAO
            ORDER BY VALOR_TOTAL DESC";
            $st = $this->pdo->prepare($sql);
            $st->execute([$dataIni, $dataFim]);
            return $st->fetchAll(\PDO::FETCH_ASSOC);
        });
    }

    public function itensMaisCaros(string $dataIni, string $dataFim, int $limite = 15): array
    {
        $key = "compras_itens_caros_{$dataIni}_{$dataFim}_{$limite}";
        return Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $limite) {
            $sql = "SELECT FIRST {$limite}
                TRIM(CAST(m.DESCRICAO AS VARCHAR(120))) AS MARCA,
                TRIM(CAST(i.DESCRICAO AS VARCHAR(120))) AS ITEM,
                i.COD_ITEM,
                MAX(ni.PRECO_UNIT)         AS PRECO_MAX,
                AVG(ni.PRECO_UNIT)         AS PRECO_MEDIO,
                SUM(ni.QTD)                AS QTD_TOTAL,
                COUNT(DISTINCT ni.NR_NOTA) AS QTD_NOTAS
            FROM NOTAS_ITENS ni
            JOIN TIPO_OPERACAO t ON ni.COD_TIPO_OPERACAO = t.COD_TIPO_OPERACAO
            JOIN ITENS i ON ni.COD_ITEM = i.COD_ITEM
            LEFT JOIN MARCA m ON m.COD_MARCA = i.COD_MARCA
            WHERE t.TIPO_NATUREZA = 'C'
              AND ni.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
              AND ni.PRECO_UNIT > 0
            GROUP BY TRIM(CAST(m.DESCRICAO AS VARCHAR(120))),
                     TRIM(CAST(i.DESCRICAO AS VARCHAR(120))),
                     i.COD_ITEM
            ORDER BY PRECO_MAX DESC";
            $st = $this->pdo->prepare($sql);
            $st->execute([$dataIni, $dataFim]);
            $rows = $st->fetchAll(\PDO::FETCH_ASSOC);
            // garantir strings UTF-8 limpas (charset Firebird pode ser WIN1252)
            foreach ($rows as &$r) {
                foreach ($r as $k => $v) {
                    if (is_string($v) && !mb_check_encoding($v, 'UTF-8')) {
                        $r[$k] = mb_convert_encoding($v, 'UTF-8', 'Windows-1252');
                    }
                }
            }
            return $rows;
        });
    }

    public function comprasVendasMensal(int $ano): array
    {
        $ttl = $ano == (int)date('Y') ? 1800 : 86400;
        return Cache::lembrar("compras_mensal_{$ano}", $ttl, function () use ($ano) {
            // BETWEEN usa índice em DATA_EMISSAO; EXTRACT(YEAR)=? bloqueia índice
            $ini = "{$ano}-01-01"; $fim = "{$ano}-12-31";
            $sqlC = "SELECT EXTRACT(MONTH FROM n.DATA_EMISSAO) MES, SUM(n.TOTAL_NOTA) TOTAL
                     FROM NOTAS_CAB n JOIN TIPO_OPERACAO t ON n.COD_TIPO_OPERACAO = t.COD_TIPO_OPERACAO
                     WHERE n.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE) AND t.TIPO_NATUREZA = 'C' AND (n.COD_SITUACAO IS NULL OR n.COD_SITUACAO <> 3)
                     GROUP BY EXTRACT(MONTH FROM n.DATA_EMISSAO)";
            $sqlV = "SELECT EXTRACT(MONTH FROM n.DATA_EMISSAO) MES, SUM(n.TOTAL_NOTA) TOTAL
                     FROM NOTAS_CAB n JOIN TIPO_OPERACAO t ON n.COD_TIPO_OPERACAO = t.COD_TIPO_OPERACAO
                     WHERE n.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE) AND t.TIPO_NATUREZA = 'V' AND (n.COD_SITUACAO IS NULL OR n.COD_SITUACAO <> 3)
                     GROUP BY EXTRACT(MONTH FROM n.DATA_EMISSAO)";
            $stC = $this->pdo->prepare($sqlC); $stC->execute([$ini, $fim]);
            $stV = $this->pdo->prepare($sqlV); $stV->execute([$ini, $fim]);
            $compras = array_column($stC->fetchAll(\PDO::FETCH_ASSOC), 'TOTAL', 'MES');
            $vendas  = array_column($stV->fetchAll(\PDO::FETCH_ASSOC), 'TOTAL', 'MES');
            $result = [];
            for ($m = 1; $m <= 12; $m++) {
                $result[] = [
                    'MES'     => $m,
                    'COMPRAS' => (float)($compras[$m] ?? 0),
                    'VENDAS'  => (float)($vendas[$m] ?? 0),
                ];
            }
            return $result;
        });
    }

    public function anosDisponiveis(): array
    {
        // Cache 1 hora — anos disponíveis mudam raramente
        return Cache::lembrar('compras_anos_disp', 3600, function () {
            $anoIni = (int)date('Y') - 4;
            $ini = "{$anoIni}-01-01"; $fim = date('Y') . '-12-31';
            $st = $this->pdo->prepare("SELECT DISTINCT EXTRACT(YEAR FROM n.DATA_EMISSAO) ANO
                    FROM NOTAS_CAB n JOIN TIPO_OPERACAO t ON n.COD_TIPO_OPERACAO = t.COD_TIPO_OPERACAO
                    WHERE t.TIPO_NATUREZA = 'C'
                      AND n.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
                    ORDER BY ANO DESC");
            $st->execute([$ini, $fim]);
            return array_column($st->fetchAll(\PDO::FETCH_ASSOC), 'ANO');
        });
    }

    // ── Evolucao diaria de compras ──────────────────────────────────────────
    public function evolucaoDiaria(string $dataIni, string $dataFim): array
    {
        $key = "compras_evol_diaria_{$dataIni}_{$dataFim}";
        return Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            try {
                $st = $this->pdo->prepare("
                    SELECT
                        n.DATA_EMISSAO                      AS DATA,
                        COUNT(DISTINCT n.NR_NOTA)           AS QTD_NOTAS,
                        COALESCE(SUM(n.TOTAL_NOTA), 0)     AS TOTAL,
                        COALESCE(AVG(n.TOTAL_NOTA), 0)     AS TICKET_MEDIO,
                        COALESCE(SUM(n.DESCONTO), 0)       AS DESCONTO
                    FROM NOTAS_CAB n
                    JOIN TIPO_OPERACAO t ON t.COD_TIPO_OPERACAO = n.COD_TIPO_OPERACAO
                                       AND t.TIPO_NATUREZA = 'C'
                    WHERE n.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
                      AND (n.COD_SITUACAO IS NULL OR n.COD_SITUACAO <> 3)
                    GROUP BY n.DATA_EMISSAO
                    ORDER BY n.DATA_EMISSAO
                ");
                $st->execute([$dataIni, $dataFim]);
                $rows = $st->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as &$r) {
                    if (isset($r['DATA']) && $r['DATA'] instanceof \DateTime) {
                        $r['DATA'] = $r['DATA']->format('Y-m-d');
                    } elseif (isset($r['DATA']) && is_string($r['DATA'])) {
                        $r['DATA'] = substr($r['DATA'], 0, 10);
                    }
                }
                return $rows;
            } catch (\Throwable) { return []; }
        });
    }

    // ── Compras por dia da semana ───────────────────────────────────────────
    public function comprasPorDiaSemana(string $dataIni, string $dataFim): array
    {
        $key = "compras_dia_sem_{$dataIni}_{$dataFim}";
        return Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            try {
                $st = $this->pdo->prepare("
                    SELECT
                        EXTRACT(WEEKDAY FROM n.DATA_EMISSAO)   AS DIA_SEM,
                        COALESCE(SUM(n.TOTAL_NOTA), 0)        AS TOTAL,
                        COUNT(DISTINCT n.NR_NOTA)               AS QTD_NOTAS,
                        COALESCE(AVG(n.TOTAL_NOTA), 0)        AS TICKET_MEDIO
                    FROM NOTAS_CAB n
                    JOIN TIPO_OPERACAO t ON t.COD_TIPO_OPERACAO = n.COD_TIPO_OPERACAO
                                       AND t.TIPO_NATUREZA = 'C'
                    WHERE n.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
                      AND (n.COD_SITUACAO IS NULL OR n.COD_SITUACAO <> 3)
                    GROUP BY EXTRACT(WEEKDAY FROM n.DATA_EMISSAO)
                    ORDER BY DIA_SEM
                ");
                $st->execute([$dataIni, $dataFim]);
                return $st->fetchAll(\PDO::FETCH_ASSOC);
            } catch (\Throwable) { return []; }
        });
    }

    // ── Fornecedores com detalhes (ticket, desconto, dias sem comprar) ───────
    public function fornecedoresDetalhado(string $dataIni, string $dataFim): array
    {
        $key = "compras_forn_det_{$dataIni}_{$dataFim}";
        return Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            try {
                $st = $this->pdo->prepare("
                    SELECT FIRST 60
                        COALESCE(TRIM(p.NOME), '(Sem Fornecedor)') AS FORNECEDOR,
                        COUNT(DISTINCT n.NR_NOTA)                    AS QTD_NOTAS,
                        COALESCE(SUM(n.TOTAL_NOTA), 0)             AS TOTAL_COMPRAS,
                        COALESCE(AVG(n.TOTAL_NOTA), 0)             AS TICKET_MEDIO,
                        COALESCE(SUM(n.DESCONTO), 0)               AS TOTAL_DESCONTO,
                        MAX(n.DATA_EMISSAO)                         AS ULTIMA_COMPRA,
                        MIN(n.DATA_EMISSAO)                         AS PRIMEIRA_COMPRA
                    FROM NOTAS_CAB n
                    JOIN TIPO_OPERACAO t ON t.COD_TIPO_OPERACAO = n.COD_TIPO_OPERACAO
                                       AND t.TIPO_NATUREZA = 'C'
                    LEFT JOIN PESSOAS p ON p.COD_PESSOA = n.COD_PESSOA
                    WHERE n.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
                      AND (n.COD_SITUACAO IS NULL OR n.COD_SITUACAO <> 3)
                    GROUP BY TRIM(p.NOME)
                    ORDER BY SUM(n.TOTAL_NOTA) DESC
                ");
                $st->execute([$dataIni, $dataFim]);
                $rows = $st->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as &$r) {
                    foreach (['ULTIMA_COMPRA', 'PRIMEIRA_COMPRA'] as $f) {
                        if (isset($r[$f]) && $r[$f] instanceof \DateTime) {
                            $r[$f] = $r[$f]->format('Y-m-d');
                        } elseif (isset($r[$f]) && is_string($r[$f])) {
                            $r[$f] = substr($r[$f], 0, 10);
                        }
                    }
                    if (!mb_check_encoding((string)($r['FORNECEDOR'] ?? ''), 'UTF-8')) {
                        $r['FORNECEDOR'] = mb_convert_encoding((string)$r['FORNECEDOR'], 'UTF-8', 'Windows-1252');
                    }
                }
                return $rows;
            } catch (\Throwable) { return []; }
        });
    }

    // ── Total de vendas no mesmo periodo (para calcular CMV%) ───────────────
    public function totalVendasPeriodo(string $dataIni, string $dataFim): float
    {
        $key = "compras_vend_periodo_{$dataIni}_{$dataFim}";
        $v = Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            try {
                $st = $this->pdo->prepare("
                    SELECT COALESCE(SUM(n.TOTAL_NOTA), 0)
                    FROM NOTAS_CAB n
                    JOIN TIPO_OPERACAO t ON t.COD_TIPO_OPERACAO = n.COD_TIPO_OPERACAO
                                       AND t.TIPO_NATUREZA = 'V'
                    WHERE n.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
                      AND (n.COD_SITUACAO IS NULL OR n.COD_SITUACAO <> 3)
                      AND n.TOTAL_NOTA > 0
                ");
                $st->execute([$dataIni, $dataFim]);
                return (float)$st->fetchColumn();
            } catch (\Throwable) { return 0.0; }
        });
        return is_numeric($v) ? (float)$v : 0.0;
    }

    // ── Top itens por quantidade comprada ───────────────────────────────────
    public function itensMaisCompradosQtd(string $dataIni, string $dataFim, int $limite = 20): array
    {
        $key = "compras_itens_qtd_{$dataIni}_{$dataFim}_{$limite}";
        return Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $limite) {
            try {
                $st = $this->pdo->prepare("
                    SELECT FIRST {$limite}
                        ni.COD_ITEM,
                        TRIM(i.DESCRICAO)              AS DESCRICAO,
                        SUM(ni.QTD)                    AS QTD_TOTAL,
                        COALESCE(SUM(ni.QTD * ni.PRECO_UNIT), 0) AS VALOR_TOTAL,
                        COALESCE(AVG(ni.PRECO_UNIT), 0) AS PRECO_MEDIO,
                        COUNT(DISTINCT ni.NR_NOTA)      AS QTD_NOTAS
                    FROM NOTAS_ITENS ni
                    JOIN TIPO_OPERACAO t ON ni.COD_TIPO_OPERACAO = t.COD_TIPO_OPERACAO
                                       AND t.TIPO_NATUREZA = 'C'
                    JOIN ITENS i ON ni.COD_ITEM = i.COD_ITEM
                    WHERE ni.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
                    GROUP BY ni.COD_ITEM, TRIM(i.DESCRICAO)
                    ORDER BY SUM(ni.QTD) DESC
                ");
                $st->execute([$dataIni, $dataFim]);
                $rows = $st->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as &$r) {
                    if (is_string($r['DESCRICAO'] ?? '') && !mb_check_encoding($r['DESCRICAO'], 'UTF-8')) {
                        $r['DESCRICAO'] = mb_convert_encoding($r['DESCRICAO'], 'UTF-8', 'Windows-1252');
                    }
                }
                return $rows;
            } catch (\Throwable) { return []; }
        });
    }

    // Compara preco medio dos top itens no periodo atual vs periodo anterior de mesmo tamanho
    public function variacaoPreco(string $dataIni, string $dataFim, int $limite = 20): array
    {
        $key = "compras_var_preco_{$dataIni}_{$dataFim}_{$limite}";
        return Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $limite) {
            try {
                // Calcula periodo anterior de mesmo tamanho
                $d1   = new \DateTime($dataIni);
                $d2   = new \DateTime($dataFim);
                $dias = (int)$d1->diff($d2)->days + 1;
                $antFim = (clone $d1)->modify('-1 day')->format('Y-m-d');
                $antIni = (clone $d1)->modify('-'.$dias.' days')->format('Y-m-d');

                // Periodo atual: top itens por valor
                $st = $this->pdo->prepare("
                    SELECT FIRST {$limite}
                        ni.COD_ITEM,
                        TRIM(i.DESCRICAO)              AS DESCRICAO,
                        AVG(ni.PRECO_UNIT)             AS PRECO_ATUAL,
                        SUM(ni.QTD * ni.PRECO_UNIT)    AS VALOR_TOTAL,
                        SUM(ni.QTD)                    AS QTD_ATUAL,
                        COUNT(DISTINCT ni.NR_NOTA)     AS NOTAS_ATUAL
                    FROM NOTAS_ITENS ni
                    JOIN TIPO_OPERACAO t ON ni.COD_TIPO_OPERACAO = t.COD_TIPO_OPERACAO
                                       AND t.TIPO_NATUREZA = 'C'
                    JOIN ITENS i ON ni.COD_ITEM = i.COD_ITEM
                    WHERE ni.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
                      AND ni.PRECO_UNIT > 0
                    GROUP BY ni.COD_ITEM, TRIM(i.DESCRICAO)
                    ORDER BY SUM(ni.QTD * ni.PRECO_UNIT) DESC
                ");
                $st->execute([$dataIni, $dataFim]);
                $atual = $st->fetchAll(\PDO::FETCH_ASSOC);
                if (empty($atual)) return [];

                // Precos do periodo anterior para os mesmos itens
                $codes = array_column($atual, 'COD_ITEM');
                $ph    = implode(',', array_fill(0, count($codes), '?'));
                $st2   = $this->pdo->prepare("
                    SELECT ni.COD_ITEM, AVG(ni.PRECO_UNIT) AS PRECO_ANT
                    FROM NOTAS_ITENS ni
                    JOIN TIPO_OPERACAO t ON ni.COD_TIPO_OPERACAO = t.COD_TIPO_OPERACAO
                                       AND t.TIPO_NATUREZA = 'C'
                    WHERE ni.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
                      AND ni.PRECO_UNIT > 0
                      AND ni.COD_ITEM IN ({$ph})
                    GROUP BY ni.COD_ITEM
                ");
                $st2->execute(array_merge([$antIni, $antFim], $codes));
                $ant = array_column($st2->fetchAll(\PDO::FETCH_ASSOC), 'PRECO_ANT', 'COD_ITEM');

                $result = [];
                foreach ($atual as $row) {
                    $desc = (string)($row['DESCRICAO'] ?? '');
                    if (!mb_check_encoding($desc, 'UTF-8')) {
                        $desc = mb_convert_encoding($desc, 'UTF-8', 'Windows-1252');
                    }
                    $precoAnt   = isset($ant[$row['COD_ITEM']]) ? (float)$ant[$row['COD_ITEM']] : null;
                    $precoAtual = (float)$row['PRECO_ATUAL'];
                    $varPct     = ($precoAnt !== null && $precoAnt > 0)
                                  ? ($precoAtual - $precoAnt) / $precoAnt * 100
                                  : null;
                    $result[] = [
                        'COD_ITEM'     => $row['COD_ITEM'],
                        'DESCRICAO'    => $desc,
                        'PRECO_ATUAL'  => $precoAtual,
                        'PRECO_ANT'    => $precoAnt,
                        'VARIACAO_PCT' => $varPct,
                        'VALOR_TOTAL'  => (float)$row['VALOR_TOTAL'],
                        'QTD_ATUAL'    => (float)$row['QTD_ATUAL'],
                        'NOTAS_ATUAL'  => (int)$row['NOTAS_ATUAL'],
                    ];
                }
                return $result;
            } catch (\Throwable) { return []; }
        });
    }

    // Itens comprados de 2+ fornecedores distintos — para comparativo de preco
    public function comparativoFornecedorItem(string $dataIni, string $dataFim): array
    {
        $key = "compras_comp_forn_{$dataIni}_{$dataFim}";
        return Cache::lembrar($key, $this->ttl($dataFim), function () use ($dataIni, $dataFim) {
            try {
                $st = $this->pdo->prepare("
                    SELECT
                        ni.COD_ITEM,
                        TRIM(i.DESCRICAO)                              AS DESCRICAO,
                        COALESCE(TRIM(p.NOME), '(Sem Fornecedor)')     AS FORNECEDOR,
                        AVG(ni.PRECO_UNIT)                             AS PRECO_MEDIO,
                        MIN(ni.PRECO_UNIT)                             AS PRECO_MIN,
                        MAX(ni.PRECO_UNIT)                             AS PRECO_MAX,
                        SUM(ni.QTD)                                    AS QTD_TOTAL,
                        COUNT(DISTINCT ni.NR_NOTA)                     AS QTD_NOTAS
                    FROM NOTAS_ITENS ni
                    JOIN TIPO_OPERACAO t ON ni.COD_TIPO_OPERACAO = t.COD_TIPO_OPERACAO
                                       AND t.TIPO_NATUREZA = 'C'
                    JOIN ITENS i ON ni.COD_ITEM = i.COD_ITEM
                    JOIN NOTAS_CAB nc ON nc.NR_NOTA = ni.NR_NOTA
                    LEFT JOIN PESSOAS p ON p.COD_PESSOA = nc.COD_PESSOA
                    WHERE ni.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
                      AND ni.PRECO_UNIT > 0
                      AND (nc.COD_SITUACAO IS NULL OR nc.COD_SITUACAO <> 3)
                    GROUP BY ni.COD_ITEM, TRIM(i.DESCRICAO), TRIM(p.NOME)
                    ORDER BY ni.COD_ITEM, AVG(ni.PRECO_UNIT) ASC
                ");
                $st->execute([$dataIni, $dataFim]);
                $rows = $st->fetchAll(\PDO::FETCH_ASSOC);

                // Agrupa por item e filtra itens com 2+ fornecedores
                $grouped = [];
                foreach ($rows as $r) {
                    $cod = $r['COD_ITEM'];
                    if (!isset($grouped[$cod])) {
                        $desc = (string)($r['DESCRICAO'] ?? '');
                        if (!mb_check_encoding($desc, 'UTF-8')) {
                            $desc = mb_convert_encoding($desc, 'UTF-8', 'Windows-1252');
                        }
                        $grouped[$cod] = ['COD_ITEM' => $cod, 'DESCRICAO' => $desc, 'FORNECEDORES' => []];
                    }
                    $forn = (string)($r['FORNECEDOR'] ?? '');
                    if (!mb_check_encoding($forn, 'UTF-8')) {
                        $forn = mb_convert_encoding($forn, 'UTF-8', 'Windows-1252');
                    }
                    $grouped[$cod]['FORNECEDORES'][] = [
                        'FORNECEDOR'  => $forn,
                        'PRECO_MEDIO' => (float)$r['PRECO_MEDIO'],
                        'PRECO_MIN'   => (float)$r['PRECO_MIN'],
                        'PRECO_MAX'   => (float)$r['PRECO_MAX'],
                        'QTD_TOTAL'   => (float)$r['QTD_TOTAL'],
                        'QTD_NOTAS'   => (int)$r['QTD_NOTAS'],
                    ];
                }
                // Apenas itens com 2+ fornecedores distintos, top 20 por qtd de fornecedores
                $result = array_filter($grouped, function($g){ return count($g['FORNECEDORES']) >= 2; });
                usort($result, function($a,$b){ return count($b['FORNECEDORES']) - count($a['FORNECEDORES']); });
                return array_values(array_slice($result, 0, 20));
            } catch (\Throwable) { return []; }
        });
    }

    // =========================================================================
    // MARGEM POR ITEM — Compra × Venda (snapshot de ITENS_ESTOQUE × vendas no período)
    // -------------------------------------------------------------------------
    // Cruza ITENS_ESTOQUE.PRECO_VENDA × custo efetivo (CUSTO_COMPRA preferido,
    // fallback CUSTO_FINAL → CUSTO_MEDIO — mesmo pattern de
    // SugestaoCompraRepository::base() L369-373) e enriquece com vendas reais
    // do período (qtd e R$ a partir de NOTAS_ITENS / TIPO_NATUREZA='V').
    //
    // Sem nova query "pesada" de varredura por linha: 1 query de catálogo
    // (ITENS_ESTOQUE × ITENS × MARCA) + 1 query agregada de vendas por COD_ITEM.
    // Tudo o que muda no filtro (faixa/busca/marca/ordem) é PHP — não invalida
    // o cache do banco.
    //
    // Flags por linha (espelham produtos/semMovimento.php L135-136 e a paleta
    // de /estoque/margem):
    //   SEM_CUSTO        — custo efetivo <= 0 (bar/distribuidora sem cadastro)
    //   CUSTO_SUSPEITO   — custo > 100000 ou custo > preco*50 (caixa-fechada plantada)
    //   PREJUIZO         — preco > 0, custo > 0 e preco < custo
    //   MARGEM_BAIXA     — markup < 20% (e sem prejuízo/sem custo)
    //   FAIXA            — sem_custo | suspeito | prejuizo | apertada | saudavel | alta
    //
    // Cache: chave por empresa + hash do período/limite (sem filtros visuais).
    // TTL 600s (memória do projeto, igual outras telas de Compras estáticas).
    //
    // Depto de Engenharia e Automação - Master Automação - Autor: Derlivon Silva
    // =========================================================================
    public function itensMargem(string $dataIni, string $dataFim, string $filtro = 'todos', int $limite = 300): array
    {
        $emp = $this->codEmpresa() ?: 0;
        $key = "compras_margem_v1_{$emp}_{$dataIni}_{$dataFim}_{$limite}";

        // Parte que vai ao banco — independente de filtro visual (cacheada).
        $base = Cache::lembrar($key, 600, function () use ($emp, $dataIni, $dataFim, $limite) {
            $utf = fn($s) => ($s !== null && $s !== '' && !mb_check_encoding((string)$s, 'UTF-8'))
                ? mb_convert_encoding((string)$s, 'UTF-8', 'Windows-1252') : (string)$s;

            // Cap absoluto pra proteger memória — usa o solicitado x2 (sobra pra filtro)
            $cap = min(5000, max($limite, 1000));
            $filtroEmp   = $emp ? "AND ie.COD_EMPRESA = {$emp}" : '';
            $filtroEmpNI = $emp ? "AND ni.COD_EMPRESA = {$emp}" : '';

            // 1) Snapshot do mix com custo + preço + marca (reaproveita 95% de EstoqueRepository::margem())
            //    Ordena por MARGEM_PERC ASC pra prejuízos virem primeiro.
            $rowsCat = [];
            try {
                $sql = "SELECT FIRST {$cap}
                    ie.COD_ITEM,
                    TRIM(i.DESCRICAO)              AS DESCRICAO,
                    TRIM(i.COD_UNIDADE)            AS COD_UNIDADE,
                    TRIM(COALESCE(m.DESCRICAO,'')) AS MARCA,
                    ie.QTD,
                    ie.CUSTO_COMPRA,
                    ie.CUSTO_FINAL,
                    ie.CUSTO_MEDIO,
                    ie.PRECO_VENDA,
                    ie.DT_ATUALIZACAO
                FROM ITENS_ESTOQUE ie
                JOIN ITENS i      ON ie.COD_ITEM  = i.COD_ITEM
                LEFT JOIN MARCA m ON m.COD_MARCA  = i.COD_MARCA
                WHERE ie.PRECO_VENDA > 0
                  {$filtroEmp}
                ORDER BY ie.PRECO_VENDA DESC";
                $rowsCat = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
            } catch (\Throwable) { $rowsCat = []; }

            // 2) Vendas reais no período (qtd e R$) por COD_ITEM
            //    Pattern: NOTAS_ITENS tem DATA_EMISSAO + COD_EMPRESA próprios (indexado).
            //    Operações 115/231/297 = vendas reais (mesmo critério de cmvMensal L521).
            $vendasMap = [];
            try {
                $sqlV = "SELECT ni.COD_ITEM,
                                COALESCE(SUM(ni.QTD),0)               AS QTD_VENDIDA,
                                COALESCE(SUM(ni.QTD*ni.PRECO_UNIT),0) AS VLR_VENDIDO,
                                COALESCE(SUM(ni.QTD*ni.CUSTO_FINAL),0) AS CMV_VENDIDO
                         FROM NOTAS_ITENS ni
                         WHERE ni.DATA_EMISSAO BETWEEN CAST(? AS DATE) AND CAST(? AS DATE)
                           AND ni.COD_TIPO_OPERACAO IN (115,231,297)
                           {$filtroEmpNI}
                         GROUP BY ni.COD_ITEM";
                $st = $this->pdo->prepare($sqlV);
                $st->execute([$dataIni, $dataFim]);
                foreach ($st->fetchAll(\PDO::FETCH_ASSOC) as $r) {
                    $vendasMap[(int)$r['COD_ITEM']] = [
                        'qtd' => (float)$r['QTD_VENDIDA'],
                        'vlr' => (float)$r['VLR_VENDIDO'],
                        'cmv' => (float)$r['CMV_VENDIDO'],
                    ];
                }
            } catch (\Throwable) { $vendasMap = []; }

            // 3) Pós-processa em PHP — encoding + flags + venda
            $out = [];
            foreach ($rowsCat as $r) {
                $cod   = (int)$r['COD_ITEM'];
                $pv    = (float)$r['PRECO_VENDA'];
                $cc    = (float)($r['CUSTO_COMPRA'] ?? 0);
                $cf    = (float)($r['CUSTO_FINAL']  ?? 0);
                $cm    = (float)($r['CUSTO_MEDIO']  ?? 0);
                // Custo efetivo: primeiroPositivo(CUSTO_COMPRA, CUSTO_FINAL, CUSTO_MEDIO)
                $custo = $cc > 0 ? $cc : ($cf > 0 ? $cf : ($cm > 0 ? $cm : 0));

                $semCusto       = ($custo <= 0);
                $custoSuspeito  = (!$semCusto) && ($custo > 100000 || $custo > $pv * 50);
                $prejuizo       = ($pv > 0 && $custo > 0 && !$custoSuspeito && $pv < $custo);
                $margemUnit     = $pv - $custo;
                $markupCusto    = ($custo > 0) ? (($pv - $custo) / $custo) * 100 : 0;     // markup s/ custo
                $margemPerc     = ($pv > 0)    ? (($pv - $custo) / $pv) * 100 : 0;        // margem s/ venda
                $margemBaixa    = (!$semCusto && !$prejuizo && !$custoSuspeito && $markupCusto < 20);

                $faixa = 'saudavel';
                if ($semCusto)           $faixa = 'sem_custo';
                elseif ($custoSuspeito)  $faixa = 'suspeito';
                elseif ($prejuizo)       $faixa = 'prejuizo';
                elseif ($markupCusto < 20)  $faixa = 'apertada';
                elseif ($markupCusto > 200) $faixa = 'alta';

                $v   = $vendasMap[$cod] ?? ['qtd'=>0,'vlr'=>0,'cmv'=>0];
                $dt  = $r['DT_ATUALIZACAO'] ?? null;
                if ($dt instanceof \DateTime) $dt = $dt->format('Y-m-d');
                elseif (is_string($dt))       $dt = substr($dt, 0, 10);

                $out[] = [
                    'COD_ITEM'           => $cod,
                    'DESCRICAO'          => $utf($r['DESCRICAO']),
                    'COD_UNIDADE'        => $utf($r['COD_UNIDADE']),
                    'MARCA'              => $utf($r['MARCA']),
                    'QTD_ESTOQUE'        => (float)$r['QTD'],
                    'CUSTO_COMPRA'       => $cc,
                    'CUSTO_FINAL'        => $cf,
                    'CUSTO_MEDIO'        => $cm,
                    'CUSTO_EFETIVO'      => $custo,
                    'PRECO_VENDA'        => $pv,
                    'MARGEM_UNIT'        => $margemUnit,
                    'MARGEM_PERC'        => $margemPerc,
                    'MARKUP_PERC'        => $markupCusto,
                    'QTD_VENDIDA'        => $v['qtd'],
                    'VLR_VENDIDO'        => $v['vlr'],
                    'CMV_VENDIDO'        => $v['cmv'],
                    'MARGEM_VALOR_VENDA' => $v['vlr'] - $v['cmv'],
                    'VALOR_ESTOQUE_VENDA'=> (float)$r['QTD'] * $pv,
                    'VALOR_ESTOQUE_CUSTO'=> (float)$r['QTD'] * $custo,
                    'LUCRO_POTENCIAL'    => (float)$r['QTD'] * max(0, $margemUnit),
                    'SEM_CUSTO'          => $semCusto,
                    'CUSTO_SUSPEITO'     => $custoSuspeito,
                    'PREJUIZO'           => $prejuizo,
                    'MARGEM_BAIXA'       => $margemBaixa,
                    'FAIXA'              => $faixa,
                    'DT_ATUALIZACAO'     => $dt,
                ];
            }
            return $out;
        });

        // Filtro de faixa (em PHP — não invalida cache)
        $filtrado = $base;
        if ($filtro === 'prejuizo')   $filtrado = array_values(array_filter($base, fn($r) => $r['PREJUIZO']));
        elseif ($filtro === 'baixa')  $filtrado = array_values(array_filter($base, fn($r) => $r['MARGEM_BAIXA']));
        elseif ($filtro === 'lucro')  $filtrado = array_values(array_filter($base, fn($r) => !$r['PREJUIZO'] && !$r['SEM_CUSTO'] && !$r['CUSTO_SUSPEITO']));
        elseif ($filtro === 'sem_custo') $filtrado = array_values(array_filter($base, fn($r) => $r['SEM_CUSTO']));

        // Ordena: prejuízos primeiro (por R$ perdido), depois margem menor
        usort($filtrado, function ($a, $b) {
            $pa = $a['PREJUIZO'] ? 0 : ($a['MARGEM_BAIXA'] ? 1 : ($a['SEM_CUSTO'] ? 3 : 2));
            $pb = $b['PREJUIZO'] ? 0 : ($b['MARGEM_BAIXA'] ? 1 : ($b['SEM_CUSTO'] ? 3 : 2));
            if ($pa !== $pb) return $pa <=> $pb;
            // Dentro da mesma faixa: maior valor de estoque/venda primeiro
            return ($b['VALOR_ESTOQUE_VENDA'] ?? 0) <=> ($a['VALOR_ESTOQUE_VENDA'] ?? 0);
        });

        return array_slice($filtrado, 0, $limite);
    }

    /**
     * KPIs agregados de margem no período — derivados do mesmo dataset de itensMargem().
     *
     *  markup_medio        — média ponderada de markup % pelo VLR_VENDIDO no período (mais real que estoque)
     *  margem_total_rs     — Σ (PV - custo) * QTD_VENDIDA
     *  qtd_baixa_margem    — SKUs com markup<20% que tiveram venda no período
     *  qtd_prejuizo        — SKUs vendidos com PV<CUSTO (perdeu dinheiro em cada unidade)
     *  rs_vazado           — Σ (CUSTO - PV) * QTD_VENDIDA dos itens em prejuízo (vermelho gigante)
     *  cmv_pct             — Σ CMV_VENDIDO / Σ VLR_VENDIDO * 100 (CMV consolidado %)
     *  cmv_confiavel       — flag agregada do helper VendasRepository::cmvConfiavel()
     *  pct_sem_custo       — % do mix com PV>0 mas sem custo cadastrado (alerta de credibilidade)
     *
     * Depto de Engenharia e Automação - Master Automação - Autor: Derlivon Silva
     */
    public function kpisMargem(string $dataIni, string $dataFim): array
    {
        try {
            // Reaproveita o dataset completo (mesma chave de cache → 0 query nova quando já carregado)
            $itens = $this->itensMargem($dataIni, $dataFim, 'todos', 5000);

            $mTopVlr = 0.0; $mTopMkPxQ = 0.0;   // numerador/denominador do markup ponderado por venda
            $margemTotal = 0.0;
            $cmvTotal = 0.0; $vendaTotal = 0.0;
            $qtdBaixa = 0; $qtdPrejuizo = 0; $rsVazado = 0.0;
            $totalItens = count($itens); $semCusto = 0; $totalComPv = 0;

            foreach ($itens as $r) {
                if (!$r['SEM_CUSTO']) $totalComPv++;
                if ($r['SEM_CUSTO'])  $semCusto++;
                $vendido = (float)$r['VLR_VENDIDO'];
                if ($vendido > 0 && !$r['SEM_CUSTO'] && !$r['CUSTO_SUSPEITO']) {
                    $mTopVlr   += $vendido;
                    $mTopMkPxQ += $vendido * (float)$r['MARKUP_PERC'];
                    $margemTotal += (float)$r['MARGEM_VALOR_VENDA'];
                }
                $cmvTotal   += (float)$r['CMV_VENDIDO'];
                $vendaTotal += (float)$r['VLR_VENDIDO'];

                $teveVenda = ((float)$r['QTD_VENDIDA'] > 0);
                if ($teveVenda && $r['MARGEM_BAIXA']) $qtdBaixa++;
                if ($teveVenda && $r['PREJUIZO']) {
                    $qtdPrejuizo++;
                    $rsVazado += ((float)$r['CUSTO_EFETIVO'] - (float)$r['PRECO_VENDA']) * (float)$r['QTD_VENDIDA'];
                }
            }

            $markupMedio = $mTopVlr > 0 ? ($mTopMkPxQ / $mTopVlr) : 0.0;
            $cmvPct      = $vendaTotal > 0 ? ($cmvTotal / $vendaTotal) * 100 : 0.0;
            $pctSemCusto = $totalItens > 0 ? ($semCusto / $totalItens) * 100 : 0.0;

            // Flag agregada de confiabilidade do CMV (helper já consagrado no VendasRepository)
            $cmvConfiavel = true;
            try {
                $vendasRepo = new \App\Repositories\VendasRepository($this->pdo);
                $anoSel = (int)substr($dataFim, 0, 4);
                $cmvArr = $vendasRepo->cmvMensal($anoSel);
                $venArr = $vendasRepo->vendasMensais($anoSel);
                $cmvConfiavel = $vendasRepo->cmvConfiavel($cmvArr, $venArr);
            } catch (\Throwable) {}

            return [
                'markup_medio'     => $markupMedio,
                'margem_total_rs'  => $margemTotal,
                'qtd_baixa_margem' => $qtdBaixa,
                'qtd_prejuizo'     => $qtdPrejuizo,
                'rs_vazado'        => $rsVazado,
                'cmv_pct'          => $cmvPct,
                'cmv_total'        => $cmvTotal,
                'venda_total'      => $vendaTotal,
                'cmv_confiavel'    => $cmvConfiavel,
                'pct_sem_custo'    => $pctSemCusto,
                'total_itens'      => $totalItens,
                'total_com_pv'     => $totalComPv,
            ];
        } catch (\Throwable) {
            return [
                'markup_medio'=>0,'margem_total_rs'=>0,'qtd_baixa_margem'=>0,'qtd_prejuizo'=>0,
                'rs_vazado'=>0,'cmv_pct'=>0,'cmv_total'=>0,'venda_total'=>0,
                'cmv_confiavel'=>true,'pct_sem_custo'=>0,'total_itens'=>0,'total_com_pv'=>0,
            ];
        }
    }

    public function ultimaData(): string
    {
        // Cache 10 minutos — MAX+JOIN é muito mais rápido que IN subquery
        $cached = Cache::lembrar('compras_ultima_data', 600, function () {
            try {
                // JOIN é 57x mais rápido que IN subquery no Firebird para MAX(DATA_EMISSAO)
                // Filtro de situação removido: só precisamos da data, não do valor — a
                // nota cancelada mais recente e a válida mais recente têm a mesma data
                // Janela recente (120d) — MAX sem limite custa ~45s no Firebird 2.5 sem índice descendente.
                $r = $this->pdo->query(
                    "SELECT MAX(n.DATA_EMISSAO)
                     FROM NOTAS_CAB n
                     JOIN TIPO_OPERACAO t ON n.COD_TIPO_OPERACAO = t.COD_TIPO_OPERACAO
                                        AND t.TIPO_NATUREZA = 'C'
                     WHERE n.DATA_EMISSAO >= DATEADD(DAY,-120,CURRENT_DATE)
                       AND n.DATA_EMISSAO <= CURRENT_DATE"
                )->fetchColumn();
                if (!$r) return date('Y-m-d');
                if ($r instanceof \DateTime) return $r->format('Y-m-d');
                return substr((string)$r, 0, 10);
            } catch (\Throwable) { return date('Y-m-d'); }
        });
        return is_string($cached) ? $cached : date('Y-m-d');
    }
}
