<?php
// =============================================================================
// Depto de Engenharia e Automacao - Master Automacao - Autor: Derlivon Silva
// =============================================================================

namespace App\Controllers;

use App\Core\Lojas;
use App\Core\Database;
use App\Core\View;
use App\Core\Cache;
use App\Core\FbAuth;
use App\Repositories\DashboardRepository;
use App\Repositories\RecebiveisRepository;

/**
 * Visão CONSOLIDADA — "Todas as lojas".
 * Faturamento / vendas / ticket de cada loja (do período de referência) + totais.
 * Conecta em cada loja (Database::conectarLoja) e usa DashboardRepository por loja.
 */
class ConsolidadoController
{
    public function index(): void
    {
        // Tela faz queries em N lojas (algumas via VPN) — quando o cache é
        // invalidado, a primeira request precisa rebuildar tudo. 30s padrao
        // do PHP nao da. Bump pra 180s + memoria 512M.
        @set_time_limit(180);
        @ini_set('memory_limit', '512M');

        // ===== v1.7.145 — PAGE CACHE DO CONSOLIDADO ==============================
        // Motivo: essa tela e a mais pesada do BI (itera N lojas via Radmin VPN, 7
        // pontos de conectarLoja, ZERO Cache::lembrar). Cliente reclamou "Resumo
        // Geral demora". Sem tocar no loop, cacheamos o HTML final por 60s.
        // - 1a request pos-warmup ainda paga o preco (rebuild)
        // - Requests seguintes ate 60s: retorna HTML pronto em ~5ms
        // - Combina com Cache-Control stale-while-revalidate=300 do View.php:
        //     browser cachea localmente ate 5min sem pedir; entre 5 e 10min pede
        //     e a resposta ja vem do cache PHP daqui em ms.
        // Chave scoped por sessao (evita vazar entre usuarios no mesmo host) +
        // querystring (p, data_ini, data_fim, view). Metodo GET so.
        // Depto de Engenharia e Automacao - Master Automacao - Autor: Derlivon Silva
        if (($_SERVER['REQUEST_METHOD'] ?? 'GET') === 'GET') {
            $sessId  = session_id() ?: 'anon';
            $qs      = $_SERVER['QUERY_STRING'] ?? '';
            // Assinatura da sessao (loja ativa + consolidado + usuario) pra invalidar
            // cache se usuario trocar de loja / entrar/sair do modo consolidado.
            $sig     = md5(($_SESSION['pbi_fb_usuario'] ?? '') . '|'
                          . (Lojas::consolidado() ? '1' : '0') . '|'
                          . (Lojas::idAtiva() ?? '') . '|'
                          . ($_SESSION['cons_periodo']  ?? '') . '|'
                          . ($_SESSION['cons_data_ini'] ?? '') . '|'
                          . ($_SESSION['cons_data_fim'] ?? ''));
            $chave   = 'consolidado_html_' . substr(md5($sessId), 0, 8) . '_' . $sig . '_' . md5($qs);
            $cached  = Cache::get($chave);
            if (is_string($cached) && $cached !== '') {
                // Bate cache: entrega HTML pronto e sai.  Header X-BI-Cache identifica.
                if (!headers_sent()) {
                    header('Content-Type: text/html; charset=UTF-8');
                    header('Cache-Control: private, max-age=60, stale-while-revalidate=300');
                    header('Vary: Cookie');
                    header('X-BI-Cache: HIT');
                }
                echo $cached;
                return;
            }
            // Cache miss: captura output pra guardar.
            ob_start();
            // Guarda flag pra saber que precisa gravar no fim (ver render() ao final).
            $GLOBALS['__consolidado_cache_key'] = $chave;
        }
        // ==========================================================================


        // single-loja: mostra a tela com 1 painel (mesmo layout, mesma estrutura).
        // multi-loja: ao acessar /consolidado direto, ATIVA o modo "Todas as lojas"
        // automaticamente — assim o link "Resumo Geral" da sidebar funciona sem
        // exigir que o usuário troque no seletor primeiro.
        if (Lojas::multi() && !Lojas::consolidado()) {
            Lojas::setConsolidado(true);
        }

        $hoje = date('Y-m-d');

        // Data custom (date picker) tem prioridade sobre o atalho de período
        $iniCustom = $_GET['data_ini'] ?? null;
        $fimCustom = $_GET['data_fim'] ?? null;
        $ehCustom  = false;
        if ($iniCustom && preg_match('/^\d{4}-\d{2}-\d{2}$/', $iniCustom)
            && $fimCustom && preg_match('/^\d{4}-\d{2}-\d{2}$/', $fimCustom)) {
            if ($fimCustom < $iniCustom) $fimCustom = $iniCustom;
            $ini = $iniCustom; $fim = $fimCustom;
            $periodo = 'custom';
            $ehCustom = true;
            $_SESSION['cons_periodo']  = 'custom';
            $_SESSION['cons_data_ini'] = $ini;
            $_SESSION['cons_data_fim'] = $fim;
        } else {
            // Período rápido: hoje | semana | mês | ano (botões na tela). Persiste na sessão.
            $periodo = $_GET['p'] ?? ($_SESSION['cons_periodo'] ?? 'mes');
            if (!in_array($periodo, ['hoje', 'semana', 'mes', 'ano', 'custom'], true)) $periodo = 'mes';
            // Se sessão tinha custom mas usuário não passou GET, recupera as datas salvas
            if ($periodo === 'custom') {
                $ini = $_SESSION['cons_data_ini'] ?? date('Y-m-01');
                $fim = $_SESSION['cons_data_fim'] ?? $hoje;
            } else {
                $_SESSION['cons_periodo'] = $periodo;
                switch ($periodo) {
                    case 'hoje':   $ini = $hoje; break;
                    case 'semana': $ini = date('Y-m-d', strtotime('-6 days')); break;
                    case 'ano':    $ini = date('Y-01-01'); break;
                    default:       $periodo = 'mes'; $ini = date('Y-m-01');
                }
                $fim = $hoje;
            }
        }

        // ---- LIBERA A TRAVA DA SESSAO ANTES DO TRABALHO PESADO ----------------
        // O PHP trava o arquivo de sessao do session_start() ate o fim do script.
        // Esta tela roda ate 180s (set_time_limit acima), e nesse tempo TODA outra
        // requisicao do mesmo usuario fica parada na fila do bootstrap - inclusive
        // os AJAX que a propria pagina dispara (/api/alertas/count,
        // /api/lojas/status, /api/compras/sugestao/ruptura-count). Como elas ainda
        // nao chegaram no controller, carregam o limite padrao de 30s e morrem com
        // "Maximum execution time of 30 seconds exceeded in Core/Application.php".
        // Resultado pro usuario: tela girando pra sempre, na rede interna e externa.
        // Daqui pra baixo so ha LEITURA de banco - nada mais escreve em $_SESSION.
        if (session_status() === PHP_SESSION_ACTIVE) { session_write_close(); }
        // ----------------------------------------------------------------------

        $linhas       = [];
        $totFat       = 0.0;
        $totVendas    = 0;
        // Agregados Com Nota / Sem Nota (soma de todas as lojas no período)
        $totValCom    = 0.0;
        $totValSem    = 0.0;
        $totQtdCom    = 0;
        $totQtdSem    = 0;
        // Sangrias (LANC_TESOURARIA SANGRIA_SUPRIM='S') — retirada operacional do caixa
        $totSangrias    = 0.0;
        $totSangriasQtd = 0;
        $totAcrescimo    = 0.0;
        $totAcrescimoQtd = 0;
        $acrescimoDetalheCons = [];
        // Detalhe consolidado das sangrias (1 linha por retirada, com a loja identificada)
        $sangriasDetalheCons = [];

        // Acumuladores das 5 features novas (somam contribuição de cada loja)
        $aggGrupos       = []; // [cod => ['nome','faturamento','qtd_notas','qtd_itens']]
        $aggMarcas       = []; // idem
        $aggFormas       = []; // [grupo => ['VALOR','QTD','FORMAS'=>[]]]
        // Totais consolidados das formas de pagamento (somam contribuição de cada loja).
        // - formas_total_geral_cons  = espelha relatório fiscal ERP (todas as ops)
        // - formas_total_vendas_cons = bate com faturamento (filtroVenda aplicado)
        // - formas_total_vendas_disp = false se ALGUMA loja não conseguiu calcular
        //   (qualquer falha invalida o total agregado — view esconde linha)
        $formasTotalGeralCons  = 0.0;
        $formasTotalVendasCons = 0.0;
        $formasTotalVendasDisp = true;
        // Drill-down consolidado da "diferença" do card de Formas de Pagamento.
        // Agrega por (operacao + forma) somando valor e qtd das lojas.
        //   chave canônica = UPPER(operacao) . '|' . UPPER(forma)
        $formasDetalheForaVendaCons = []; // [chave => ['operacao','forma','qtd','valor']]
        // OBS: data_ini_ant/data_fim_ant ficam IDENTICAS pra todas as lojas
        // (sao funcao do periodo $ini..$fim, nao da loja). Por isso so atribuimos
        // uma vez (na 1a loja que conseguir calcular) e ja vale pro consolidado.
        $compSemana      = ['atual' => 0.0, 'anterior' => 0.0, 'vendas_atual' => 0, 'vendas_anterior' => 0,
                            'data_ini_ant' => null, 'data_fim_ant' => null];
        $compMes         = ['atual' => 0.0, 'anterior' => 0.0, 'vendas_atual' => 0, 'vendas_anterior' => 0,
                            'data_ini_ant' => null, 'data_fim_ant' => null];
        $compAno         = ['atual' => 0.0, 'anterior' => 0.0, 'vendas_atual' => 0, 'vendas_anterior' => 0,
                            'data_ini_ant' => null, 'data_fim_ant' => null];
        $compMesmoDataAA = ['atual' => 0.0, 'anterior' => 0.0, 'vendas_atual' => 0, 'vendas_anterior' => 0,
                            'data_ini_ant' => null, 'data_fim_ant' => null];

        $linhaCSNzero = [
            'com_nota' => ['qtd' => 0, 'valor' => 0.0],
            'sem_nota' => ['qtd' => 0, 'valor' => 0.0],
        ];

        // v1.5.49 — Alertas de "R$ X vendidos a prazo sem titulo no ERP" agregados por loja.
        // Threshold do metodo garante silencio pra quem nao tem problema (bar/varejo a vista).
        // Universal: qualquer segmento que caia no criterio (>=R$100k a prazo, <20% em RP) ve.
        $alertasSemTituloCons = [];

        // Em single-loja, monta uma "loja virtual" usando config/database.php
        // pra reusar o mesmo loop sem precisar duplicar código.
        if (Lojas::multi()) {
            // Filtra: só lojas com status='ativa'. Pré-abertura e inativa
            // saem do consolidado (cliente avisa via banner separado).
            // Lojas VIRTUAIS (com faixa_preco) entram e são somadas via
            // LinhaPrecoRepository — enquanto data_abertura='pendente' contribuem
            // R$0, então NÃO duplicam a loja física irmã. — Master Automação
            $lojasIterar = Lojas::ativas();
        } else {
            $cfgSingle = require dirname(__DIR__, 2) . '/config/database.php';
            $lojasIterar = ['_single' => [
                'nome'        => $cfgSingle['empresa']    ?? 'Loja Principal',
                'cod_empresa' => (int)($cfgSingle['cod_empresa'] ?? 0),
                'principal'   => true,
            ]];
        }

        foreach ($lojasIterar as $id => $l) {
            $id  = (string) $id;
            $cod = (int) ($l['cod_empresa'] ?? 0);
            $row = [
                'id'       => $id,
                'nome'     => $l['nome'] ?? $id,
                'fat'      => 0.0,
                'vendas'   => 0,
                'ticket'   => 0.0,
                'erro'     => null,
                // Tipo do erro de conexão: 'offline' | 'login' | 'banco' | 'desconhecido'
                // null = sem erro. View usa pra escolher cor/ícone do card de erro.
                'erro_tipo' => null,
                'ultima'   => null,
                'com_nota' => ['qtd' => 0, 'valor' => 0.0],
                'sem_nota' => ['qtd' => 0, 'valor' => 0.0],
                // Sangrias do caixa (LANC_TESOURARIA SANGRIA_SUPRIM='S')
                'sangrias' => ['total' => 0.0, 'qtd' => 0],
                // Lista detalhada de cada sangria (data/hora, usuario, motivo, valor)
                'sangrias_detalhe' => [],
                // dados por loja (top 5/10 + comparativos)
                'grupos_loja'           => [],
                'marcas_loja'           => [],
                'formas_pgto_loja'      => [],
                // 2 totais "ricos" da forma de pagamento desta loja:
                //   total_geral  = espelha relatório fiscal (inclui crediário/cortesia/troca)
                //   total_vendas = bate com card de Faturamento (filtroVenda aplicado)
                'formas_total_geral'    => 0.0,
                'formas_total_vendas'   => null,
                // Drill-down da diferença (1 linha por TIPO_OPERACAO × FORMA fora do filtroVenda).
                // [] se a query falhou ou loja não tem filtroVenda → view esconde toggle.
                'formas_detalhe_fora_venda' => [],
                'comp_semana_loja'      => ['atual' => 0.0, 'anterior' => 0.0, 'pct' => 0.0, 'abs' => 0.0,
                                            'data_ini_ant' => null, 'data_fim_ant' => null],
                'comp_mes_loja'         => ['atual' => 0.0, 'anterior' => 0.0, 'pct' => 0.0, 'abs' => 0.0,
                                            'data_ini_ant' => null, 'data_fim_ant' => null],
                'comp_ano_loja'         => ['atual' => 0.0, 'anterior' => 0.0, 'pct' => 0.0, 'abs' => 0.0,
                                            'data_ini_ant' => null, 'data_fim_ant' => null],
                'comp_mesma_data_aa_loja' => ['atual' => 0.0, 'anterior' => 0.0, 'pct' => 0.0, 'abs' => 0.0,
                                              'data_ini_ant' => null, 'data_fim_ant' => null],
                // 4 indicadores extras por loja (v1.4.32+) — dia da semana, hora,
                // top produtos, top clientes. Cada lista pode ficar vazia se a
                // loja não devolveu dado (banco antigo, schema diferente etc.).
                'dia_semana_loja' => [],
                'hora_loja'       => [],
                'produtos_loja'   => [],
                'clientes_loja'   => [],
            ];

            try {
                Lojas::setFoco($id); // alinha o escopo de cache à loja consultada
                $pdo  = Database::conectarLoja($id);
                // v1.7.111 — CRITICO: $repo criado AQUI, no topo do try, pra garantir
                // que sempre tem PDO da loja ATUAL. Antes (v1.7.106+) o auto-detect
                // faixasIrmas pulava a criacao e $repo ficava com PDO da iteracao
                // anterior (bug: sangrias/comSemNota da tamara apareciam no ach_20a).
                $repo = new DashboardRepository($pdo, $fim, $cod);

                // Loja VIRTUAL (faixa_preco): faturamento vem item-level do
                // LinhaPrecoRepository, respeitando data_abertura. Enquanto for
                // 'pendente' entra zerada e marca 'nao_abriu' (view mostra
                // banner "em preparação").
                //
                // v1.7.107 — REMOVIDO o 'continue' antigo. Antes pulávamos TODOS os
                // KPIs adicionais (grupos/marcas/hora/produtos/formas) porque
                // "não se aplicam" à linha de preço. Resultado: painel da loja
                // virtual aparecia com todos os cards vazios. Preferimos agora
                // mostrar os KPIs do BANCO COMPLETO (não filtrados por R$10) com
                // um aviso pra o usuário saber que aquilo agrega ambas as linhas.
                if (!empty($l['faixa_preco'])) {
                    $lpRepo = new \App\Repositories\LinhaPrecoRepository($pdo);
                    $res    = $lpRepo->resumoConsolidado(
                        (int) $l['faixa_preco'], $ini, $fim, $l['data_abertura'] ?? null
                    );
                    $row['fat']    = (float) $res['fat'];
                    $row['vendas'] = (int)   $res['vendas'];
                    $row['ticket'] = $row['vendas'] > 0 ? $row['fat'] / $row['vendas'] : 0.0;
                    $row['nao_abriu']         = !empty($res['naoAbriu']);
                    $row['virtual_agregada']  = true;   // sinal pra view mostrar aviso
                    $totFat    += $row['fat'];
                    $totVendas += $row['vendas'];
                    // NÃO faz continue: deixa o loop calcular os demais KPIs abaixo
                    // (agregados do banco todo). Se loja não abriu, os KPIs seguem
                    // vazios naturalmente (banco sem venda no período efetivo).
                }

                // Cálculo de fat/vendas/ticket:
                // - Se loja é virtual (faixa_preco), JÁ foi calculado acima e não sobrescreve
                // - Se tem irmãs com faixa (auto-detecção v1.7.106), usa resumoComplementar
                // - Senão, caminho tradicional (DashboardRepository)
                if (empty($l['faixa_preco'])) {
                    $faixasIrmas = Lojas::faixasCobertasPorIrmas($id);
                    if (!empty($faixasIrmas)) {
                        $lpRepo = new \App\Repositories\LinhaPrecoRepository($pdo);
                        $res = $lpRepo->resumoComplementar(
                            $faixasIrmas, $ini, $fim, $l['data_abertura'] ?? null
                        );
                        $row['fat']    = (float) $res['fat'];
                        $row['vendas'] = (int)   $res['vendas'];
                        $row['ticket'] = $row['vendas'] > 0 ? $row['fat'] / $row['vendas'] : 0.0;
                    } else {
                        // Caminho tradicional: DashboardRepository sem filtro. ($repo ja criado no topo do try)
                        $row['fat']    = (float) $repo->faturamentoIntervalo($ini, $fim);
                        $row['vendas'] = (int)   $repo->totalVendasIntervalo($ini, $fim);
                        $row['ticket'] = $row['vendas'] > 0 ? $row['fat'] / $row['vendas'] : 0.0;
                    }
                }
                // v1.7.111 — $repo agora garantido no topo do try (nao mais aqui)
                // Sem venda no período? mostra até quando a base tem dado (avisa cópia desatualizada).
                if ($row['vendas'] === 0) $row['ultima'] = $repo->ultimaVenda();

                // Com Nota / Sem Nota — try/catch isolado: se a query nova falhar (banco
                // mais antigo, schema diferente), mantemos os zeros e seguimos sem
                // derrubar o resto da linha da loja.
                try {
                    $csn = $repo->faturamentoComSemNota($ini, $fim);
                    if (is_array($csn) && isset($csn['com_nota'], $csn['sem_nota'])) {
                        $row['com_nota'] = $csn['com_nota'];
                        $row['sem_nota'] = $csn['sem_nota'];
                    }
                } catch (\Throwable) {
                    // segue com os zeros já definidos em $row
                }

                $totFat    += $row['fat'];
                $totVendas += $row['vendas'];
                $totValCom += (float) ($row['com_nota']['valor'] ?? 0);
                $totValSem += (float) ($row['sem_nota']['valor'] ?? 0);
                $totQtdCom += (int)   ($row['com_nota']['qtd']   ?? 0);
                $totQtdSem += (int)   ($row['sem_nota']['qtd']   ?? 0);

                // Sangrias do caixa — query isolada: se a tabela LANC_TESOURARIA não
                // existir no schema do cliente, mantém zeros sem quebrar o consolidado.
                try {
                    $sg = $repo->sangriasNoIntervalo($ini, $fim);
                    $row['sangrias'] = [
                        'total' => (float)($sg['total'] ?? 0),
                        'qtd'   => (int)  ($sg['qtd']   ?? 0),
                    ];
                    $totSangrias    += $row['sangrias']['total'];
                    $totSangriasQtd += $row['sangrias']['qtd'];

                    // Detalhe: 1 linha por sangria (data/hora, usuário, motivo, valor).
                    // Só pega se houve sangria — evita query desnecessária.
                    if ($row['sangrias']['total'] > 0) {
                        $det = $repo->sangriasDetalheNoIntervalo($ini, $fim, 500);
                        $row['sangrias_detalhe'] = $det;
                        // Agrega no consolidado, anotando de qual loja é cada linha
                        foreach ($det as $d) {
                            $d['loja_id']   = $id;
                            $d['loja_nome'] = $l['nome'] ?? $id;
                            $sangriasDetalheCons[] = $d;
                        }
                    }
                } catch (\Throwable) {
                    // sangrias fica em zeros — view esconde o card se total==0
                }

                // v1.5.63 — Acréscimo "invisível" (juros embutidos no TOTAL_NOTA).
                // Universal: se cliente não pratica, retorna 0 e a view esconde o card.
                try {
                    $ac = $repo->acrescimoInvisivelIntervalo($ini, $fim);
                    $row['acrescimo'] = [
                        'total'          => (float)($ac['total']          ?? 0),
                        'qtd_notas'      => (int)  ($ac['qtd_notas']      ?? 0),
                        'media_por_nota' => (float)($ac['media_por_nota'] ?? 0),
                        'pct_fat'        => (float)($ac['pct_fat']        ?? 0),
                    ];
                    $totAcrescimo    += $row['acrescimo']['total'];
                    $totAcrescimoQtd += $row['acrescimo']['qtd_notas'];

                    if ($row['acrescimo']['total'] > 0.5) {
                        $det = $repo->acrescimoInvisivelDetalhe($ini, $fim, 300);
                        $row['acrescimo_detalhe'] = $det;
                        foreach ($det as $d) {
                            $d['loja_id']   = $id;
                            $d['loja_nome'] = $l['nome'] ?? $id;
                            $acrescimoDetalheCons[] = $d;
                        }
                    }
                } catch (\Throwable) {
                    // silencioso — nunca deve derrubar consolidado
                }

                // v1.5.49 — Alerta "vendido a prazo sem titulo" por loja.
                // RecebiveisRepository.alertaVendasSemTitulo() ja tem threshold defensivo
                // (>=R$100k a prazo E <20% em RP). Bar/varejo a vista nunca dispara.
                try {
                    $repoR = new RecebiveisRepository($pdo);
                    $al    = $repoR->alertaVendasSemTitulo($cod);
                    if (!empty($al['temAlerta'])) {
                        $alertasSemTituloCons[] = [
                            'loja_id'           => $id,
                            'loja_nome'         => $l['nome'] ?? $id,
                            'vendido_prazo_12m' => (float)$al['vendido_prazo_12m'],
                            'registrado_rp_12m' => (float)$al['registrado_rp_12m'],
                            'diferenca'         => (float)$al['diferenca'],
                            'ops_problematicas' => (array)$al['ops_problematicas'],
                        ];
                    }
                } catch (\Throwable) {
                    // Se o RecebiveisRepository quebrar em alguma loja, silenciamos e seguimos.
                    // O alerta e opcional — nunca deve derrubar o consolidado inteiro.
                }

                // helper local: calcula delta (atual vs anterior) por loja
                $deltaLoja = function (float $a, float $b): array {
                    $abs = $a - $b;
                    $pct = $b > 0 ? ($a - $b) / $b * 100 : ($a > 0 ? 100.0 : 0.0);
                    return ['atual' => $a, 'anterior' => $b, 'pct' => $pct, 'abs' => $abs];
                };

                // === FEATURE 1: Vendas por Grupo (top 20 por loja + agregado) ===
                try {
                    $vg = $repo->vendasPorGrupoConsolidado($ini, $fim, 20);
                    $linhasGrupoLoja = [];
                    foreach (($vg['linhas'] ?? []) as $g) {
                        // Agregar por NOME normalizado (uppercase+trim), nao por
                        // cod_grupo: cada loja tem cadastro independente e o
                        // mesmo nome de grupo pode ter codigos diferentes em
                        // bancos diferentes. Sem isso, "UTILIDADES DOMESTICAS"
                        // de 2 lojas apareceria como 2 itens separados no top.
                        $nomeKey = mb_strtoupper(trim((string)$g['nome']));
                        $k = $nomeKey !== '' ? 'n:'.$nomeKey : 'c:'.($g['cod_grupo'] ?? 'null');
                        if (!isset($aggGrupos[$k])) {
                            $aggGrupos[$k] = ['cod_grupo' => $g['cod_grupo'], 'nome' => $g['nome'],
                                              'faturamento' => 0.0, 'qtd_notas' => 0, 'qtd_itens' => 0.0,
                                              'por_loja' => []];
                        }
                        $aggGrupos[$k]['faturamento'] += (float)$g['faturamento'];
                        $aggGrupos[$k]['qtd_notas']   += (int)$g['qtd_notas'];
                        $aggGrupos[$k]['qtd_itens']   += (float)$g['qtd_itens'];
                        // Stacked bar por loja: acumula faturamento desta loja ($id) no grupo $k
                        $aggGrupos[$k]['por_loja'][$id] = ($aggGrupos[$k]['por_loja'][$id] ?? 0.0) + (float)$g['faturamento'];
                        $linhasGrupoLoja[] = [
                            'cod_grupo'   => $g['cod_grupo'],
                            'nome'        => $g['nome'],
                            'faturamento' => (float)$g['faturamento'],
                            'qtd_notas'   => (int)$g['qtd_notas'],
                            'qtd_itens'   => (float)$g['qtd_itens'],
                        ];
                    }
                    // top 20 por faturamento (a query ja deve devolver ordenado, mas garantimos)
                    usort($linhasGrupoLoja, fn($a, $b) => $b['faturamento'] <=> $a['faturamento']);
                    $row['grupos_loja'] = array_slice($linhasGrupoLoja, 0, 20);
                } catch (\Throwable) {}

                // === FEATURE 2: Vendas por Marca (top 20 por loja + agregado) ===
                try {
                    $vm = $repo->vendasPorMarcaConsolidado($ini, $fim, 20);
                    $linhasMarcaLoja = [];
                    foreach (($vm['linhas'] ?? []) as $m) {
                        // Mesma logica: agregar por NOME normalizado em vez de
                        // cod_marca, ja que cada loja tem cadastro independente.
                        $nomeKey = mb_strtoupper(trim((string)$m['nome']));
                        $k = $nomeKey !== '' ? 'n:'.$nomeKey : 'c:'.($m['cod_marca'] ?? 'null');
                        if (!isset($aggMarcas[$k])) {
                            $aggMarcas[$k] = ['cod_marca' => $m['cod_marca'], 'nome' => $m['nome'],
                                              'faturamento' => 0.0, 'qtd_notas' => 0, 'qtd_itens' => 0.0,
                                              'por_loja' => []];
                        }
                        $aggMarcas[$k]['faturamento'] += (float)$m['faturamento'];
                        $aggMarcas[$k]['qtd_notas']   += (int)$m['qtd_notas'];
                        $aggMarcas[$k]['qtd_itens']   += (float)$m['qtd_itens'];
                        // Stacked bar por loja: acumula faturamento desta loja ($id) na marca $k
                        $aggMarcas[$k]['por_loja'][$id] = ($aggMarcas[$k]['por_loja'][$id] ?? 0.0) + (float)$m['faturamento'];
                        $linhasMarcaLoja[] = [
                            'cod_marca'   => $m['cod_marca'],
                            'nome'        => $m['nome'],
                            'faturamento' => (float)$m['faturamento'],
                            'qtd_notas'   => (int)$m['qtd_notas'],
                            'qtd_itens'   => (float)$m['qtd_itens'],
                        ];
                    }
                    usort($linhasMarcaLoja, fn($a, $b) => $b['faturamento'] <=> $a['faturamento']);
                    $row['marcas_loja'] = array_slice($linhasMarcaLoja, 0, 20);
                } catch (\Throwable) {}

                // === FEATURE 3: Formas de Pagamento — TODAS individualmente (sem "Outros") ===
                // Usa a versão "ComTotal" que devolve grupos + total_geral (espelha
                // o relatório fiscal do ERP Fire) + total_vendas (bate com o card
                // de Faturamento ao lado). A diferença entre os dois = crediário,
                // cortesia, devolução com troca etc.
                try {
                    $fp = $repo->formasPagamentoIntervaloComTotal($ini, $fim);
                    $row['formas_total_geral']  = (float) ($fp['total_geral']  ?? 0);
                    $row['formas_total_vendas'] = $fp['total_vendas'] !== null ? (float)$fp['total_vendas'] : null;
                    $formasLoja = [];
                    foreach (($fp['grupos'] ?? []) as $grp) {
                        $nomeForma = trim((string)($grp['GRUPO'] ?? ''));
                        if ($nomeForma === '') continue; // descarta forma sem nome (não cria bucket "Outros")
                        // chave canônica = nome em UPPER+TRIM (junta variantes de capitalização entre lojas)
                        $k = mb_strtoupper($nomeForma, 'UTF-8');
                        if (!isset($aggFormas[$k])) {
                            $aggFormas[$k] = ['GRUPO' => $nomeForma, 'VALOR' => 0.0, 'QTD' => 0, 'por_loja' => []];
                        }
                        $aggFormas[$k]['VALOR'] += (float)($grp['VALOR'] ?? 0);
                        $aggFormas[$k]['QTD']   += (int)($grp['QTD'] ?? 0);
                        $aggFormas[$k]['por_loja'][$id] = ($aggFormas[$k]['por_loja'][$id] ?? 0.0) + (float)($grp['VALOR'] ?? 0);
                        $formasLoja[] = [
                            'GRUPO' => $nomeForma,
                            'VALOR' => (float)($grp['VALOR'] ?? 0),
                            'QTD'   => (int)($grp['QTD'] ?? 0),
                        ];
                    }
                    // ordena e calcula PCT por loja
                    usort($formasLoja, fn($a, $b) => $b['VALOR'] <=> $a['VALOR']);
                    $sumValLoja = 0.0;
                    foreach ($formasLoja as $f) { $sumValLoja += (float)$f['VALOR']; }
                    foreach ($formasLoja as &$f) {
                        $f['PCT'] = $sumValLoja > 0 ? (float)$f['VALOR'] / $sumValLoja * 100 : 0.0;
                    }
                    unset($f);
                    $row['formas_pgto_loja'] = $formasLoja;
                    // Drill-down da diferença: 1 linha por (TIPO_OPERACAO × FORMA) fora do filtroVenda.
                    // Vai pra view por loja E acumula no agregado consolidado (somando entre lojas).
                    $detalheLoja = (array) ($fp['detalhe_fora_venda'] ?? []);
                    $row['formas_detalhe_fora_venda'] = $detalheLoja;
                    foreach ($detalheLoja as $d) {
                        $opNome = (string)($d['operacao'] ?? '');
                        $fmNome = (string)($d['forma'] ?? '');
                        if ($opNome === '' && $fmNome === '') continue;
                        $k = mb_strtoupper($opNome, 'UTF-8') . '|' . mb_strtoupper($fmNome, 'UTF-8');
                        if (!isset($formasDetalheForaVendaCons[$k])) {
                            $formasDetalheForaVendaCons[$k] = [
                                'operacao' => $opNome,
                                'forma'    => $fmNome,
                                'qtd'      => 0,
                                'valor'    => 0.0,
                            ];
                        }
                        $formasDetalheForaVendaCons[$k]['qtd']   += (int)($d['qtd'] ?? 0);
                        $formasDetalheForaVendaCons[$k]['valor'] += (float)($d['valor'] ?? 0);
                    }
                    // Acumula nos totais consolidados
                    $formasTotalGeralCons += (float)$row['formas_total_geral'];
                    if ($row['formas_total_vendas'] === null) {
                        // Loja não conseguiu calcular → invalida agregado consolidado
                        $formasTotalVendasDisp = false;
                    } else {
                        $formasTotalVendasCons += (float)$row['formas_total_vendas'];
                    }
                } catch (\Throwable) {}

                // === FEATURE 4: Comparativos (semana / mês / ano + mesma data ano anterior) ===
                // v1.7.107 — Bloqueio "loja nao existia antes":
                // Se a loja tem data_abertura E o periodo anterior comparado eh ANTES
                // dessa data, a loja nao existia — nao adianta comparar. Marca
                // 'nao_existia_antes'=true e zera anterior/vendas_anterior.
                // data_abertura chega em DD-MM-YYYY; converte pra Y-m-d pra comparar.
                $dataAb = (string)($l['data_abertura'] ?? '');
                $dataAbISO = '';
                if (preg_match('/^(\d{2})-(\d{2})-(\d{4})$/', $dataAb, $m)) {
                    $dataAbISO = "{$m[3]}-{$m[2]}-{$m[1]}";
                }
                $bloquearAnt = function(?string $iniAnt) use ($dataAbISO): bool {
                    return $dataAbISO !== '' && $iniAnt !== null && $iniAnt < $dataAbISO;
                };
                try {
                    $semAnt = $repo->faturamentoPeriodoAnterior($ini, $fim, 'semana');
                    $iniAnt = $semAnt['data_ini'] ?? null;
                    $naoExistia = $bloquearAnt($iniAnt);
                    $valAnt = $naoExistia ? 0.0 : (float)($semAnt['faturamento'] ?? 0);
                    $qtdAnt = $naoExistia ? 0   : (int)  ($semAnt['qtd_vendas']  ?? 0);
                    $compSemana['atual']           += $row['fat'];
                    $compSemana['vendas_atual']    += $row['vendas'];
                    $compSemana['anterior']        += $valAnt;
                    $compSemana['vendas_anterior'] += $qtdAnt;
                    if ($compSemana['data_ini_ant'] === null) {
                        $compSemana['data_ini_ant'] = $iniAnt;
                        $compSemana['data_fim_ant'] = $semAnt['data_fim'] ?? null;
                    }
                    $row['comp_semana_loja']       = array_merge(
                        $deltaLoja($row['fat'], $valAnt),
                        [
                            'data_ini_ant'      => $iniAnt,
                            'data_fim_ant'      => $semAnt['data_fim'] ?? null,
                            'vendas_atual'      => (int)$row['vendas'],
                            'vendas_anterior'   => $qtdAnt,
                            'nao_existia_antes' => $naoExistia,
                            'data_abertura'     => $dataAb,
                        ]
                    );
                } catch (\Throwable) {}
                try {
                    $mesAnt = $repo->faturamentoPeriodoAnterior($ini, $fim, 'mes');
                    $iniAnt = $mesAnt['data_ini'] ?? null;
                    $naoExistia = $bloquearAnt($iniAnt);
                    $valAnt = $naoExistia ? 0.0 : (float)($mesAnt['faturamento'] ?? 0);
                    $qtdAnt = $naoExistia ? 0   : (int)  ($mesAnt['qtd_vendas']  ?? 0);
                    $compMes['atual']           += $row['fat'];
                    $compMes['vendas_atual']    += $row['vendas'];
                    $compMes['anterior']        += $valAnt;
                    $compMes['vendas_anterior'] += $qtdAnt;
                    if ($compMes['data_ini_ant'] === null) {
                        $compMes['data_ini_ant'] = $iniAnt;
                        $compMes['data_fim_ant'] = $mesAnt['data_fim'] ?? null;
                    }
                    $row['comp_mes_loja']       = array_merge(
                        $deltaLoja($row['fat'], $valAnt),
                        [
                            'data_ini_ant'      => $iniAnt,
                            'data_fim_ant'      => $mesAnt['data_fim'] ?? null,
                            'vendas_atual'      => (int)$row['vendas'],
                            'vendas_anterior'   => $qtdAnt,
                            'nao_existia_antes' => $naoExistia,
                            'data_abertura'     => $dataAb,
                        ]
                    );
                } catch (\Throwable) {}
                try {
                    // YTD: acumulado do ano atual ate a data vs mesmo acumulado
                    // do ano anterior. NAO usa $row['fat'] (que e o filtro), e sim
                    // o YTD calculado direto pra ficar apples-to-apples.
                    $ytd = $repo->faturamentoYTD($ini, $fim);
                    $iniAnt = $ytd['anterior_ini'] ?? null;
                    $naoExistia = $bloquearAnt($iniAnt);
                    $valAnt = $naoExistia ? 0.0 : (float)($ytd['anterior']        ?? 0);
                    $qtdAnt = $naoExistia ? 0   : (int)  ($ytd['vendas_anterior'] ?? 0);
                    $compAno['atual']           += (float) ($ytd['atual']        ?? 0);
                    $compAno['vendas_atual']    += (int)   ($ytd['vendas_atual'] ?? 0);
                    $compAno['anterior']        += $valAnt;
                    $compAno['vendas_anterior'] += $qtdAnt;
                    if ($compAno['data_ini_ant'] === null) {
                        $compAno['data_ini_ant'] = $iniAnt;
                        $compAno['data_fim_ant'] = $ytd['anterior_fim'] ?? null;
                    }
                    $row['comp_ano_loja']       = array_merge(
                        $deltaLoja((float)($ytd['atual'] ?? 0), $valAnt),
                        [
                            'data_ini_ant'      => $iniAnt,
                            'data_fim_ant'      => $ytd['anterior_fim'] ?? null,
                            'vendas_atual'      => (int)($ytd['vendas_atual'] ?? 0),
                            'vendas_anterior'   => $qtdAnt,
                            'nao_existia_antes' => $naoExistia,
                            'data_abertura'     => $dataAb,
                        ]
                    );
                } catch (\Throwable) {}
                try {
                    $mda = $repo->faturamentoMesmaDataAnoAnterior($ini, $fim);
                    $iniAnt = $mda['data_ini'] ?? null;
                    $naoExistia = $bloquearAnt($iniAnt);
                    $valAnt = $naoExistia ? 0.0 : (float)($mda['faturamento'] ?? 0);
                    $qtdAnt = $naoExistia ? 0   : (int)  ($mda['qtd_vendas']  ?? 0);
                    $compMesmoDataAA['atual']           += $row['fat'];
                    $compMesmoDataAA['vendas_atual']    += $row['vendas'];
                    $compMesmoDataAA['anterior']        += $valAnt;
                    $compMesmoDataAA['vendas_anterior'] += $qtdAnt;
                    $compMesmoDataAA['data_ini_ant']    = $iniAnt;
                    $compMesmoDataAA['data_fim_ant']    = $mda['data_fim'] ?? null;
                    $row['comp_mesma_data_aa_loja']     = array_merge(
                        $deltaLoja($row['fat'], $valAnt),
                        [
                            'data_ini_ant'      => $iniAnt,
                            'data_fim_ant'      => $mda['data_fim'] ?? null,
                            'vendas_atual'      => (int)$row['vendas'],
                            'vendas_anterior'   => $qtdAnt,
                            'nao_existia_antes' => $naoExistia,
                            'data_abertura'     => $dataAb,
                        ]
                    );
                } catch (\Throwable) {}

                // === 4 indicadores extras por loja (cada um isolado em try/catch) ===
                // Cada chamada cai em [] se a loja não tiver schema/coluna esperada,
                // mantendo o resto do painel funcional.
                try { $row['dia_semana_loja'] = $repo->vendasPorDiaSemanaIntervalo($ini, $fim); } catch (\Throwable) {}
                try { $row['hora_loja']       = $repo->vendasPorHoraIntervalo($ini, $fim);     } catch (\Throwable) {}
                try { $row['produtos_loja']   = $repo->topProdutosIntervalo($ini, $fim, 10);   } catch (\Throwable) {}
                try { $row['clientes_loja']   = $repo->topClientesIntervalo($ini, $fim, 10);   } catch (\Throwable) {}
            } catch (\Throwable $e) {
                $msg = $e->getMessage();
                $row['erro'] = $msg;
                // Classifica heuristicamente a mensagem do PDOException pra escolher
                // cor/ícone certo na view. Cobre os erros mais comuns do Firebird/PDO:
                //   "Unable to complete network request" → host inacessível (offline)
                //   "connection refused"                  → porta fechada (FB parado)
                //   "no route to host"                    → roteamento (VPN off)
                //   "I/O error during 'open'"             → caminho do banco errado
                //   "Your user name and password are not defined" → credencial inválida
                //   "User name should be specified"       → credencial vazia
                $low = strtolower($msg);
                if (preg_match('/network|unreachable|no route|timed? ?out|connection (refused|reset|timed)|getaddrinfo|host.*(unknown|not found)|name or service not known|connection (closed|aborted)|broken pipe|connection lost/i', $low)) {
                    $row['erro_tipo'] = 'offline';
                } elseif (preg_match('/user.*(name|password)|password are not|authentication|invalid (user|credential)|access denied|not authorized|login.*failed/i', $low)) {
                    $row['erro_tipo'] = 'login';
                } elseif (preg_match('/i\/o error|database.*(not found|does not exist)|file not found|cannot open|no such file|unavailable database|access.*denied.*file/i', $low)) {
                    $row['erro_tipo'] = 'banco';
                } else {
                    $row['erro_tipo'] = 'desconhecido';
                }
            } finally {
                Lojas::setFoco(null);
            }

            $linhas[] = $row;
        }

        // Ranking por faturamento (desc)
        usort($linhas, fn($a, $b) => $b['fat'] <=> $a['fat']);

        $totValCSN      = $totValCom + $totValSem;
        $pctComNotaGlob = $totValCSN > 0 ? ($totValCom / $totValCSN) * 100.0 : 0.0;

        // === Pós-processamento das 5 features ===
        // Top 20 grupos por faturamento somado entre as lojas
        usort($aggGrupos, fn($a, $b) => $b['faturamento'] <=> $a['faturamento']);
        $gruposConsolidado = array_slice(array_values($aggGrupos), 0, 20);
        $sumGruposFat = 0.0;
        foreach ($gruposConsolidado as $g) { $sumGruposFat += $g['faturamento']; }
        foreach ($gruposConsolidado as &$g) {
            $g['participacao_pct'] = $sumGruposFat > 0 ? $g['faturamento'] / $sumGruposFat * 100 : 0.0;
            $g['ticket_medio']     = $g['qtd_notas'] > 0 ? $g['faturamento'] / $g['qtd_notas'] : 0.0;
        }
        unset($g);

        // Top 20 marcas por faturamento somado entre as lojas
        usort($aggMarcas, fn($a, $b) => $b['faturamento'] <=> $a['faturamento']);
        $marcasConsolidado = array_slice(array_values($aggMarcas), 0, 20);
        $sumMarcasFat = 0.0;
        foreach ($marcasConsolidado as $m) { $sumMarcasFat += $m['faturamento']; }
        foreach ($marcasConsolidado as &$m) {
            $m['participacao_pct'] = $sumMarcasFat > 0 ? $m['faturamento'] / $sumMarcasFat * 100 : 0.0;
            $m['ticket_medio']     = $m['qtd_notas'] > 0 ? $m['faturamento'] / $m['qtd_notas'] : 0.0;
        }
        unset($m);

        // Formas de pagamento — soma valor+qtd por categoria, ordena desc, calcula %
        usort($aggFormas, fn($a, $b) => $b['VALOR'] <=> $a['VALOR']);
        $formasPagamentoConsolidado = array_values($aggFormas);
        $sumFormasVal = 0.0;
        foreach ($formasPagamentoConsolidado as $f) { $sumFormasVal += $f['VALOR']; }
        foreach ($formasPagamentoConsolidado as &$f) {
            $f['PCT'] = $sumFormasVal > 0 ? $f['VALOR'] / $sumFormasVal * 100 : 0.0;
        }
        unset($f);

        // Drill-down da diferença consolidado: ordena por valor desc
        usort($formasDetalheForaVendaCons, fn($a, $b) => $b['valor'] <=> $a['valor']);
        $formasDetalheForaVendaCons = array_values($formasDetalheForaVendaCons);

        // Deltas dos comparativos
        $calcDelta = function (array $c): array {
            $a = (float)$c['atual']; $b = (float)$c['anterior'];
            $c['delta_abs'] = $a - $b;
            $c['delta_pct'] = $b > 0 ? ($a - $b) / $b * 100 : ($a > 0 ? 100.0 : 0.0);
            return $c;
        };
        $compSemana      = $calcDelta($compSemana);
        $compMes         = $calcDelta($compMes);
        $compAno         = $calcDelta($compAno);
        $compMesmoDataAA = $calcDelta($compMesmoDataAA);

        View::render('consolidado/index', [
            'titulo'         => 'Consolidado — Todas as Lojas',
            'linhas'         => $linhas,
            'totFat'         => $totFat,
            'totVendas'      => $totVendas,
            'totTicket'      => $totVendas > 0 ? $totFat / $totVendas : 0.0,
            'totValCom'      => $totValCom,
            'totValSem'      => $totValSem,
            'totQtdCom'      => $totQtdCom,
            'totQtdSem'      => $totQtdSem,
            // Lojas em pré-abertura (excluídas do consolidado) — banner avisa
            'lojasPreAbertura'    => Lojas::multi() ? Lojas::preAbertura() : [],
            // Sangrias (caixa) — total consolidado das lojas
            'totSangrias'         => $totSangrias,
            'totSangriasQtd'      => $totSangriasQtd,
            // v1.5.49 — alertas de "vendido a prazo sem titulo no ERP" por loja
            'alertasSemTituloCons'=> $alertasSemTituloCons,
            // Lista detalhada agregada (todas as lojas), ordenada por valor desc
            'sangriasDetalheCons' => (function() use (&$sangriasDetalheCons) {
                usort($sangriasDetalheCons, fn($a, $b) => $b['valor'] <=> $a['valor']);
                return $sangriasDetalheCons;
            })(),
            // v1.5.63 — Acréscimo "invisível" (juros embutidos no TOTAL_NOTA)
            'totAcrescimo'         => $totAcrescimo,
            'totAcrescimoQtd'      => $totAcrescimoQtd,
            'acrescimoDetalheCons' => (function() use (&$acrescimoDetalheCons) {
                usort($acrescimoDetalheCons, fn($a, $b) => $b['acrescimo'] <=> $a['acrescimo']);
                return $acrescimoDetalheCons;
            })(),
            'pctComNota'     => $pctComNotaGlob,
            'pctSemNota'     => $totValCSN > 0 ? 100.0 - $pctComNotaGlob : 0.0,
            'periodo'        => $periodo,
            'dataIni'        => $ini,
            'dataFim'        => $fim,
            // 5 features novas
            'gruposConsolidado'           => $gruposConsolidado,
            'marcasConsolidado'           => $marcasConsolidado,
            'formasPagamentoConsolidado'  => $formasPagamentoConsolidado,
            // Totais "ricos" do card de Formas de Pagamento (resolve divergência visual com Faturamento)
            'formasTotalGeralCons'        => $formasTotalGeralCons,
            'formasTotalVendasCons'       => $formasTotalVendasDisp ? $formasTotalVendasCons : null,
            // Drill-down da diferença (1 linha por TIPO_OPERACAO × FORMA fora do filtroVenda)
            'formasDetalheForaVendaCons'  => $formasDetalheForaVendaCons,
            'compSemana'                  => $compSemana,
            'compMes'                     => $compMes,
            'compAno'                     => $compAno,
            'compMesmoDataAA'             => $compMesmoDataAA,
        ]);

        // ===== v1.7.145 — Grava HTML no page cache pra proxima request bater cache
        if (!empty($GLOBALS['__consolidado_cache_key'])) {
            $html = ob_get_contents();
            // Nao interfere no buffer: apenas grava e libera pra saida normal seguir
            if (is_string($html) && $html !== '') {
                // TTL: 60s se periodo inclui hoje, 30min se fechado (dado historico nao muda)
                $ttl = (isset($fim) && $fim >= date('Y-m-d')) ? 60 : 1800;
                Cache::set($GLOBALS['__consolidado_cache_key'], $html, $ttl);
                if (!headers_sent()) header('X-BI-Cache: MISS-STORED');
            }
            unset($GLOBALS['__consolidado_cache_key']);
            ob_end_flush();
        }
    }

    /**
     * Último mês COM vendas da loja principal (1ª da lista) — usado como período
     * padrão do consolidado quando o usuário ainda não escolheu. Evita abrir num
     * mês vazio (ex.: mês corrente sem dado numa base de cópia desatualizada).
     */
    private function mesPadrao(): array
    {
        foreach (Lojas::todas() as $id => $l) {
            try {
                Lojas::setFoco((string) $id);
                $pdo  = Database::conectarLoja((string) $id);
                $repo = new DashboardRepository($pdo, null, (int) ($l['cod_empresa'] ?? 0));
                $d    = $repo->dataReferencia(); // 'YYYY-MM-DD' (última data com venda)
                Lojas::setFoco(null);
                if ($d && preg_match('/^\d{4}-\d{2}-\d{2}$/', $d)) {
                    return ['ano' => (int) substr($d, 0, 4), 'mes' => (int) substr($d, 5, 2)];
                }
            } catch (\Throwable) {
                Lojas::setFoco(null);
            }
        }
        return ['ano' => (int) date('Y'), 'mes' => (int) date('n')];
    }

    /**
     * GET /api/consolidado/pdv-fluxo — feed de vendas do PDV em tempo real.
     * Retorna as ultimas N vendas globais com forma de pagamento agregada + qtd de itens.
     */
    public function pdvFluxoApi(): void
    {
        header('Content-Type: application/json; charset=utf-8');
        @set_time_limit(60);
        try {
            if (Lojas::multi()) {
                $lojasIterar = Lojas::ativas();
            } else {
                $cfgSingle = require dirname(__DIR__, 2) . '/config/database.php';
                $lojasIterar = ['_single' => [
                    'nome'        => $cfgSingle['empresa']    ?? 'Loja Principal',
                    'cod_empresa' => (int)($cfgSingle['cod_empresa'] ?? 0),
                    'principal'   => true,
                ]];
            }

            $feedGlobal = [];
            $totalPorLoja = [];

            foreach ($lojasIterar as $id => $l) {
                $id   = (string) $id;
                $cod  = (int) ($l['cod_empresa'] ?? 0);
                $nome = $l['nome'] ?? $id;

                // v1.7.109 — Alinha escopo de cache + zera static VendasRepository::$codEmpresa.
                // Sem isso: 1a loja do loop grava static COD_EMPRESA e as demais lojas usam
                // o valor errado -> queries filtram empresa errada e retornam 0/vazio.
                // Mesmo bug ja corrigido no ConsolidadoController::index() em v1.7.104.
                Lojas::setFoco($id);

                // Loja VIRTUAL (faixa_preco): compartilha banco com uma loja física
                // irmã. Se ainda não abriu (data_abertura='pendente' ou futura),
                // NÃO puxa cupom nenhum — senão apareceria a movimentação da irmã
                // no card errado (dois cards com o mesmo R$8.076). — Master Automação
                if (!empty($l['faixa_preco'])) {
                    // data_abertura vem em DD-MM-YYYY (BR) ou 'pendente'.
                    $abertura = $l['data_abertura'] ?? null;
                    $pendente = false;
                    if (is_string($abertura)) {
                        $t = trim($abertura);
                        if (strtolower($t) === 'pendente') {
                            $pendente = true;
                        } else {
                            $iso = $t;
                            if (preg_match('/^(\d{2})-(\d{2})-(\d{4})$/', $t, $m)) $iso = "{$m[3]}-{$m[2]}-{$m[1]}";
                            if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $iso) && $iso > date('Y-m-d')) $pendente = true;
                        }
                    }
                    if ($pendente) {
                        $totalPorLoja[] = [
                            'id' => $id, 'nome' => $nome, 'fat' => 0.0, 'qtd' => 0,
                            'erro' => null, 'nao_abriu' => true,
                        ];
                        continue; // não puxa feed — evita duplicar cupom da loja irmã
                    }
                    // Loja virtual JÁ ABERTA: puxa fat/qtd item-level filtrado pelo preço.
                    // Feed detalhado por item nao existe (um cupom pode ter itens R$10 +
                    // R$20 misturados — nao ha "cupom da loja virtual"). Marca 'virtual'
                    // pra view mostrar mensagem apropriada no lugar de "sem vendas ainda".
                    try {
                        $pdo    = Database::conectarLoja($id);
                        $lpRepo = new \App\Repositories\LinhaPrecoRepository($pdo);
                        $res    = $lpRepo->resumoConsolidado((int)$l['faixa_preco'], date('Y-m-d'), date('Y-m-d'), $abertura);
                        $totalPorLoja[] = [
                            'id' => $id, 'nome' => $nome,
                            'fat' => (float)$res['fat'], 'qtd' => (int)$res['vendas'], 'erro' => null,
                            'virtual' => true, 'faixa_preco' => (int)$l['faixa_preco'],
                        ];
                    } catch (\Throwable) {
                        $totalPorLoja[] = [
                            'id' => $id, 'nome' => $nome, 'fat' => 0.0, 'qtd' => 0, 'erro' => 'offline',
                            'virtual' => true, 'faixa_preco' => (int)$l['faixa_preco'],
                        ];
                    }
                    continue;
                }

                try {
                    if (Lojas::multi()) {
                        $pdo = Database::conectarLoja($id);
                    } else {
                        $pdo = Database::conectar();
                    }
                    $repo   = new DashboardRepository($pdo, date('Y-m-d'), $cod);

                    // v1.7.109 — Auto-deteccao (mesma do consolidado v1.7.106):
                    // se essa loja fisica tem irma com faixa_preco, o KPI vira "banco - preco"
                    // pra bater com o consolidado. Feed continua puxando cupons inteiros
                    // (cupom fisico eh dela — nao ha "cupom da irma virtual").
                    $faixasIrmas = Lojas::faixasCobertasPorIrmas($id);
                    if (!empty($faixasIrmas)) {
                        $lpRepo = new \App\Repositories\LinhaPrecoRepository($pdo);
                        $res = $lpRepo->resumoComplementar($faixasIrmas, date('Y-m-d'), date('Y-m-d'), $l['data_abertura'] ?? null);
                        $kpi = ['fat' => (float)$res['fat'], 'qtd' => (int)$res['vendas'], 'ultima' => null];
                    } else {
                        $kpi = $repo->vendasAoVivo($id);
                    }

                    // Feed detalhado das ultimas N
                    $vendas = $repo->pdvFluxoRecente($id, 30);
                    foreach ($vendas as $v) {
                        if (empty($v['ts'])) continue;
                        $feedGlobal[] = array_merge($v, [
                            'loja_id'   => $id,
                            'loja_nome' => $nome,
                        ]);
                    }
                    $totalPorLoja[] = [
                        'id'   => $id,
                        'nome' => $nome,
                        'fat'  => (float) ($kpi['fat'] ?? 0),
                        'qtd'  => (int)   ($kpi['qtd'] ?? 0),
                        'erro' => null,
                    ];
                } catch (\Throwable) {
                    $totalPorLoja[] = ['id' => $id, 'nome' => $nome, 'fat' => 0.0, 'qtd' => 0, 'erro' => 'offline'];
                }
            }

            // v1.7.109 — sem SLICE GLOBAL. Antes cortava em 50 e as lojas com vendas
            // mais antigas (ex.: ach20barra 19:02 vs Mega 19:36) ficavam FORA e o JS
            // renderizava "sem vendas ainda" mesmo com KPI positivo. Cada loja ja limita
            // 30 no repo; o JS corta 12 por loja na renderizacao — nao precisa cortar aqui.
            usort($feedGlobal, fn($a, $b) => strcmp((string)$b['ts'], (string)$a['ts']));

            echo json_encode([
                'ok'      => true,
                'agora'   => date('Y-m-d H:i:s'),
                'feed'    => $feedGlobal,
                'lojas'   => $totalPorLoja,
                'total'   => count($feedGlobal),
            ], JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
        } catch (\Throwable) {
            http_response_code(500);
            echo json_encode(['ok' => false, 'erro' => 'falha']);
        }
        exit;
    }

    /**
     * GET /api/consolidado/nota-itens?loja=X&nota=Y&serie=Z&emp=W&op=V
     * Retorna itens de uma NF especifica pra expansao no PDV em tempo real.
     */
    public function notaItensApi(): void
    {
        header('Content-Type: application/json; charset=utf-8');
        @set_time_limit(30);
        try {
            $lojaId = (string) ($_GET['loja']  ?? '');
            $nrNota = (int)    ($_GET['nota']  ?? 0);
            $serie  = (string) ($_GET['serie'] ?? '');
            $codEmp = (int)    ($_GET['emp']   ?? 0);
            $codOp  = (int)    ($_GET['op']    ?? 0);
            if ($nrNota <= 0) {
                echo json_encode(['ok' => false, 'erro' => 'nota invalida']);
                exit;
            }
            if (Lojas::multi() && $lojaId !== '') {
                $pdo = Database::conectarLoja($lojaId);
            } else {
                $pdo = Database::conectar();
            }
            $itens = (new DashboardRepository($pdo, date('Y-m-d'), $codEmp))
                ->itensDaNota($nrNota, $serie, $codEmp, $codOp);
            echo json_encode([
                'ok'     => true,
                'nota'   => $nrNota,
                'itens'  => $itens,
                'total_itens' => count($itens),
            ], JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
        } catch (\Throwable) {
            http_response_code(500);
            echo json_encode(['ok' => false, 'erro' => 'falha']);
        }
        exit;
    }

    /**
     * GET /api/consolidado/ao-vivo — endpoint JSON leve para o card "AO VIVO".
     * Retorna, por loja: fat + qtd + timestamp da ultima venda de HOJE, mais o
     * status (verde/amarelo/vermelho) baseado no tempo desde a ultima venda.
     * Cache curto (20s) no repository. Usado pelo auto-refresh no consolidado/index.
     */
    public function aoVivoApi(): void
    {
        header('Content-Type: application/json; charset=utf-8');
        @set_time_limit(60);
        try {
            if (Lojas::multi()) {
                $lojasIterar = Lojas::ativas();
            } else {
                $cfgSingle = require dirname(__DIR__, 2) . '/config/database.php';
                $lojasIterar = ['_single' => [
                    'nome'        => $cfgSingle['empresa']    ?? 'Loja Principal',
                    'cod_empresa' => (int)($cfgSingle['cod_empresa'] ?? 0),
                    'principal'   => true,
                ]];
            }

            $agora = time();
            $lojas = [];
            $totFat = 0.0;
            $totQtd = 0;

            foreach ($lojasIterar as $id => $l) {
                $id  = (string) $id;
                $cod = (int) ($l['cod_empresa'] ?? 0);
                // v1.7.109 — mesmo fix do pdvFluxoApi (isolamento por loja)
                Lojas::setFoco($id);
                $item = [
                    'id'          => $id,
                    'nome'        => $l['nome'] ?? $id,
                    'fat'         => 0.0,
                    'qtd'         => 0,
                    'ultima'      => null,
                    'ultima_min'  => null,
                    'status'      => 'sem_vendas',
                    'erro'        => null,
                ];
                try {
                    if (Lojas::multi()) {
                        $pdo = Database::conectarLoja($id);
                    } else {
                        $pdo = Database::conectar();
                    }
                    // v1.7.109 — auto-deteccao pra loja fisica com irma virtual
                    if (!empty($l['faixa_preco'])) {
                        $lpRepo = new \App\Repositories\LinhaPrecoRepository($pdo);
                        $res = $lpRepo->resumoConsolidado((int)$l['faixa_preco'], date('Y-m-d'), date('Y-m-d'), $l['data_abertura'] ?? null);
                        $dados = ['fat' => (float)$res['fat'], 'qtd' => (int)$res['vendas'], 'ultima' => null];
                    } else {
                        $faixasIrmas = Lojas::faixasCobertasPorIrmas($id);
                        if (!empty($faixasIrmas)) {
                            $lpRepo = new \App\Repositories\LinhaPrecoRepository($pdo);
                            $res = $lpRepo->resumoComplementar($faixasIrmas, date('Y-m-d'), date('Y-m-d'), $l['data_abertura'] ?? null);
                            $dados = ['fat' => (float)$res['fat'], 'qtd' => (int)$res['vendas'], 'ultima' => null];
                        } else {
                            $dados = (new DashboardRepository($pdo, date('Y-m-d'), $cod))->vendasAoVivo($id);
                        }
                    }
                    $item['fat']    = (float) $dados['fat'];
                    $item['qtd']    = (int)   $dados['qtd'];
                    $item['ultima'] = $dados['ultima'] ?? null;

                    if ($item['ultima']) {
                        $ts = strtotime($item['ultima']);
                        if ($ts) {
                            $diffMin = max(0, (int) floor(($agora - $ts) / 60));
                            $item['ultima_min'] = $diffMin;
                            if ($diffMin <= 15)      $item['status'] = 'ativa';
                            elseif ($diffMin <= 30)  $item['status'] = 'atencao';
                            else                     $item['status'] = 'parada';
                        }
                    } elseif ($item['qtd'] === 0) {
                        $item['status'] = 'sem_vendas';
                    } else {
                        $item['status'] = 'sem_hora';
                    }

                    $totFat += $item['fat'];
                    $totQtd += $item['qtd'];
                } catch (\Throwable $e) {
                    $item['erro']   = 'offline';
                    $item['status'] = 'offline';
                }
                $lojas[] = $item;
            }

            echo json_encode([
                'ok'       => true,
                'agora'    => date('Y-m-d H:i:s'),
                'total'    => ['fat' => $totFat, 'qtd' => $totQtd, 'lojas' => count($lojas)],
                'lojas'    => $lojas,
            ], JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
        } catch (\Throwable $e) {
            http_response_code(500);
            echo json_encode(['ok' => false, 'erro' => 'falha']);
        }
        exit;
    }
}
