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

namespace App\Repositories;

use App\Core\Cache;

/**
 * Auditoria — análises de comportamento operacional pra investigação do gestor.
 *
 * Diferente do módulo Vendas (que mostra "como vai a operação"), aqui o foco é
 * "o que merece o olho clínico": desconto, cancelamento, devolução, vendas
 * em horário atípico, etc. Toda análise é por OPERADORA DE CAIXA (usuário que
 * abriu a sessão de caixa) — é a chave operacional que gera responsabilização.
 */
class AuditoriaRepository
{
    private static ?int $codEmpresa = null;
    private static bool $codEmpresaCarregado = false;

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

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

    /** TTL: 10 min p/ dados de hoje, 30 min pra períodos passados */
    private function ttl(string $dataFim): int
    {
        return ($dataFim >= date('Y-m-d')) ? 600 : 1800;
    }

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

    // -------------------------------------------------------------------------
    // DESCONTO POR OPERADORA
    // -------------------------------------------------------------------------

    /**
     * Desconto agregado por operadora de caixa no período.
     * Liga: NOTAS_CAB → ABERTURA_CAIXA (COD_ABERT_CAIXA) → USUARIOS (COD_USUARIO_ABERTURA)
     *
     * Retorna por operadora: qtd vendas, faturamento, desconto total, % desc/fat,
     * ticket médio, e indicador "alto" se % > média da loja.
     */
    public function descontoPorOperadora(string $dataIni, string $dataFim): array
    {
        $emp      = $this->filtroEmpresa('nc');
        $cacheKey = "audit_desconto_oper_{$dataIni}_{$dataFim}" . $this->escopoCache();

        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $emp) {
            try {
                $sql = "
                    SELECT
                        u.COD_USUARIO                                    AS COD_OPERADORA,
                        TRIM(u.NOME)                                     AS OPERADORA,
                        COUNT(*)                                         AS QTD_VENDAS,
                        COALESCE(SUM(CAST(nc.TOTAL_NOTA AS DOUBLE PRECISION)), 0)  AS FATURADO,
                        COALESCE(SUM(CAST(nc.DESCONTO   AS DOUBLE PRECISION)), 0)  AS DESCONTO,
                        COUNT(CASE WHEN nc.DESCONTO > 0 THEN 1 END)      AS QTD_COM_DESCONTO
                    FROM NOTAS_CAB nc
                    JOIN ABERTURA_CAIXA ac ON ac.COD_ABERT_CAIXA = nc.COD_ABERT_CAIXA
                    JOIN USUARIOS u        ON u.COD_USUARIO     = ac.COD_USUARIO_ABERTURA
                    WHERE nc.DATA_EMISSAO BETWEEN '{$dataIni}' AND '{$dataFim}'
                      AND nc.DATA_CANC IS NULL
                      {$emp}
                    GROUP BY u.COD_USUARIO, TRIM(u.NOME)
                    ORDER BY DESCONTO DESC, FATURADO DESC
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);

                // Normalização + enriquecimento
                $totFat = 0.0; $totDesc = 0.0;
                foreach ($rows as $r) {
                    $totFat  += (float)$r['FATURADO'];
                    $totDesc += (float)$r['DESCONTO'];
                }
                $mediaPct = $totFat > 0 ? ($totDesc / $totFat * 100) : 0;

                $out = [];
                foreach ($rows as $r) {
                    $fat   = (float)$r['FATURADO'];
                    $desc  = (float)$r['DESCONTO'];
                    $pct   = $fat > 0 ? ($desc / $fat * 100) : 0;
                    $qtd   = (int)$r['QTD_VENDAS'];
                    $qtdCD = (int)$r['QTD_COM_DESCONTO'];
                    $nome  = (string)$r['OPERADORA'];
                    if (!mb_check_encoding($nome, 'UTF-8')) {
                        $nome = mb_convert_encoding($nome, 'UTF-8', 'Windows-1252');
                    }
                    $out[] = [
                        'cod_operadora'    => (string)$r['COD_OPERADORA'], // COD_USUARIO no Firebird é login (string)
                        'operadora'        => $nome,
                        'qtd_vendas'       => $qtd,
                        'faturado'         => round($fat, 2),
                        'desconto'         => round($desc, 2),
                        'qtd_com_desconto' => $qtdCD,
                        'ticket_medio'     => $qtd > 0 ? round($fat / $qtd, 2) : 0.0,
                        'pct_desconto'     => round($pct, 4),
                        'acima_media'      => ($mediaPct > 0 && $pct > $mediaPct),
                    ];
                }

                return [
                    'lista'      => $out,
                    'total_fat'  => round($totFat, 2),
                    'total_desc' => round($totDesc, 2),
                    'media_pct'  => round($mediaPct, 4),
                ];
            } catch (\Throwable $e) {
                return ['lista' => [], 'total_fat' => 0.0, 'total_desc' => 0.0, 'media_pct' => 0.0, 'erro' => $e->getMessage()];
            }
        });
    }

    // -------------------------------------------------------------------------
    // LIBERAÇÕES DE CAIXA  (cartão de liberação do supervisor)
    // -------------------------------------------------------------------------

    /** Mapa amigável dos tipos de liberação */
    public static function tiposLiberacao(): array
    {
        return [
            0 => ['nome' => 'Forma de Pagamento',  'icon' => 'bi-credit-card-2-back',     'cor' => '#3b82f6'],
            1 => ['nome' => 'Desconto',            'icon' => 'bi-percent',                'cor' => '#f59e0b'],
            2 => ['nome' => 'Fatura Atrasada',     'icon' => 'bi-calendar-x',             'cor' => '#dc2626'],
            3 => ['nome' => 'Limite de Crédito',   'icon' => 'bi-bank',                   'cor' => '#8b5cf6'],
        ];
    }

    /**
     * Liberações de caixa agrupadas por SOLICITANTE (operadora que pediu).
     * Filtro opcional por tipo (0/1/2/3) ou null para todos.
     */
    public function liberacoesPorOperadora(string $dataIni, string $dataFim, ?int $tipo = null): array
    {
        $emp      = $this->filtroEmpresa('l');
        $tipoSql  = $tipo !== null ? "AND l.TIPO_LIBERACAO = " . (int)$tipo : '';
        // v3: separa SUM DESCONTO por categoria — TIPO=1 (desconto real, dinheiro que saiu)
        // x TIPO IN (2,3) (autorizado: fatura atrasada/limite credito — NAO e saida).
        // Antes somavamos tudo junto, inflando ~23x o valor real (R$ 36.705 vs R$ 1.602).
        // Ticket Desconto = SUM(DESCONTO TIPO=1) / COUNT(TIPO=1), nunca divide pelo total.
        // ALERTA agora calibrado em cima de Desconto R$, nao mais do valor inflado.
        $cacheKey = "audit_liberacoes_oper_v3_{$dataIni}_{$dataFim}_" . ($tipo ?? 'all') . $this->escopoCache();

        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $emp, $tipoSql) {
            try {
                // (A) SOLICITANTES — agregado por operadora de caixa
                $sql = "
                    SELECT
                        TRIM(l.COD_USUARIO_SOLICITANTE)   AS SOLICITANTE,
                        COUNT(*)                          AS QTD_TOTAL,
                        COUNT(CASE WHEN l.TIPO_LIBERACAO=0 THEN 1 END) AS QTD_PGTO,
                        COUNT(CASE WHEN l.TIPO_LIBERACAO=1 THEN 1 END) AS QTD_DESC,
                        COUNT(CASE WHEN l.TIPO_LIBERACAO=2 THEN 1 END) AS QTD_FATURA,
                        COUNT(CASE WHEN l.TIPO_LIBERACAO=3 THEN 1 END) AS QTD_CRED,
                        COALESCE(SUM(CASE WHEN l.TIPO_LIBERACAO=1
                                          THEN CAST(l.DESCONTO AS DOUBLE PRECISION) ELSE 0 END), 0) AS VAL_DESC_REAL,
                        COALESCE(SUM(CASE WHEN l.TIPO_LIBERACAO IN (2,3)
                                          THEN CAST(l.DESCONTO AS DOUBLE PRECISION) ELSE 0 END), 0) AS VAL_AUTORIZADO,
                        COALESCE(SUM(CAST(l.DESCPERM AS DOUBLE PRECISION)), 0) AS VAL_PERMIT_SOLIC,
                        COUNT(CASE WHEN CAST(SUBSTRING(CAST(l.HORA AS VARCHAR(8)) FROM 1 FOR 2) AS INTEGER) BETWEEN 0 AND 5  THEN 1 END) AS QTD_MADRUGADA,
                        COUNT(CASE WHEN CAST(SUBSTRING(CAST(l.HORA AS VARCHAR(8)) FROM 1 FOR 2) AS INTEGER) BETWEEN 6 AND 11 THEN 1 END) AS QTD_MANHA,
                        COUNT(CASE WHEN CAST(SUBSTRING(CAST(l.HORA AS VARCHAR(8)) FROM 1 FOR 2) AS INTEGER) BETWEEN 12 AND 17 THEN 1 END) AS QTD_TARDE,
                        COUNT(CASE WHEN CAST(SUBSTRING(CAST(l.HORA AS VARCHAR(8)) FROM 1 FOR 2) AS INTEGER) BETWEEN 18 AND 23 THEN 1 END) AS QTD_NOITE
                    FROM LIBERACOES l
                    WHERE l.DATA BETWEEN '{$dataIni}' AND '{$dataFim}'
                      AND l.COD_USUARIO_SOLICITANTE IS NOT NULL
                      {$emp}
                      {$tipoSql}
                    GROUP BY TRIM(l.COD_USUARIO_SOLICITANTE)
                    ORDER BY QTD_TOTAL DESC
                ";
                $rowsS = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);

                // (B) SUPERVISORES — agregado por quem liberou
                // v3: idem (A) — separa Desconto R$ (TIPO=1) e Autorizado R$ (TIPO IN 2,3)
                $sqlSup = "
                    SELECT
                        TRIM(l.COD_USUARIO_LIBEROU) AS SUPERVISOR,
                        COUNT(*) AS QTD,
                        COUNT(CASE WHEN l.TIPO_LIBERACAO=1 THEN 1 END) AS QTD_DESC,
                        COALESCE(SUM(CASE WHEN l.TIPO_LIBERACAO=1
                                          THEN CAST(l.DESCONTO AS DOUBLE PRECISION) ELSE 0 END), 0) AS VAL_DESC_REAL,
                        COALESCE(SUM(CASE WHEN l.TIPO_LIBERACAO IN (2,3)
                                          THEN CAST(l.DESCONTO AS DOUBLE PRECISION) ELSE 0 END), 0) AS VAL_AUTORIZADO,
                        COALESCE(SUM(CAST(l.DESCPERM AS DOUBLE PRECISION)), 0) AS VAL_PERMIT,
                        COALESCE(SUM(CASE WHEN l.TIPO_LIBERACAO=1
                                          THEN CAST(l.DESCONTO AS DOUBLE PRECISION) - CAST(l.DESCPERM AS DOUBLE PRECISION)
                                          ELSE 0 END), 0) AS SPREAD_AUTORIZADO,
                        COUNT(CASE WHEN CAST(SUBSTRING(CAST(l.HORA AS VARCHAR(8)) FROM 1 FOR 2) AS INTEGER) BETWEEN 0 AND 5  THEN 1 END) AS QTD_MADRUGADA,
                        COUNT(CASE WHEN CAST(SUBSTRING(CAST(l.HORA AS VARCHAR(8)) FROM 1 FOR 2) AS INTEGER) BETWEEN 6 AND 11 THEN 1 END) AS QTD_MANHA,
                        COUNT(CASE WHEN CAST(SUBSTRING(CAST(l.HORA AS VARCHAR(8)) FROM 1 FOR 2) AS INTEGER) BETWEEN 12 AND 17 THEN 1 END) AS QTD_TARDE,
                        COUNT(CASE WHEN CAST(SUBSTRING(CAST(l.HORA AS VARCHAR(8)) FROM 1 FOR 2) AS INTEGER) BETWEEN 18 AND 23 THEN 1 END) AS QTD_NOITE
                    FROM LIBERACOES l
                    WHERE l.DATA BETWEEN '{$dataIni}' AND '{$dataFim}'
                      AND l.COD_USUARIO_LIBEROU IS NOT NULL
                      {$emp}
                      {$tipoSql}
                    GROUP BY TRIM(l.COD_USUARIO_LIBEROU)
                    ORDER BY QTD DESC
                ";
                $rowsSup = $this->pdo->query($sqlSup)->fetchAll(\PDO::FETCH_ASSOC);

                // (C) PIVOT OP x SUP — alimenta os dois cards (vinculo conluio)
                $sqlPivot = "
                    SELECT
                        TRIM(l.COD_USUARIO_SOLICITANTE) AS OP,
                        TRIM(l.COD_USUARIO_LIBEROU)     AS SUP,
                        COUNT(*) AS Q,
                        COALESCE(SUM(CAST(l.DESCONTO AS DOUBLE PRECISION)), 0) AS V
                    FROM LIBERACOES l
                    WHERE l.DATA BETWEEN '{$dataIni}' AND '{$dataFim}'
                      AND l.COD_USUARIO_SOLICITANTE IS NOT NULL
                      AND l.COD_USUARIO_LIBEROU IS NOT NULL
                      {$emp}
                      {$tipoSql}
                    GROUP BY 1, 2
                ";
                $rowsPivot = $this->pdo->query($sqlPivot)->fetchAll(\PDO::FETCH_ASSOC);

                // helper utf-8
                $u = function(string $s): string {
                    return mb_check_encoding($s, 'UTF-8')
                        ? $s
                        : mb_convert_encoding($s, 'UTF-8', 'Windows-1252');
                };

                // Indexa pivot: por operador -> [sup => qtd] e por supervisor -> [op => qtd]
                $pivotPorOp  = [];
                $pivotPorSup = [];
                foreach ($rowsPivot as $p) {
                    $op  = $u(trim((string)$p['OP']));
                    $sup = $u(trim((string)$p['SUP']));
                    $q   = (int)$p['Q'];
                    if ($op === '' || $sup === '') continue;
                    $pivotPorOp[$op][$sup]  = ($pivotPorOp[$op][$sup]  ?? 0) + $q;
                    $pivotPorSup[$sup][$op] = ($pivotPorSup[$sup][$op] ?? 0) + $q;
                }

                // Totais gerais
                // v3: ticket medio da loja agora e calculado em cima de DESCONTO R$ real (TIPO=1)
                // dividido por QTD DE DESCONTOS (TIPO=1), nao mais por SUM(DESCONTO) total / COUNT total.
                $totalGeral = 0;
                $totaisPorTipo = [0=>0, 1=>0, 2=>0, 3=>0];
                $totDescReal     = 0.0; // SUM(DESCONTO) WHERE TIPO=1 — dinheiro que efetivamente saiu
                $totAutorizado   = 0.0; // SUM(DESCONTO) WHERE TIPO IN (2,3) — limite/fatura autorizado
                $totQtdDescGeral = 0;
                foreach ($rowsS as $r) {
                    $totalGeral       += (int)$r['QTD_TOTAL'];
                    $totaisPorTipo[0] += (int)$r['QTD_PGTO'];
                    $totaisPorTipo[1] += (int)$r['QTD_DESC'];
                    $totaisPorTipo[2] += (int)$r['QTD_FATURA'];
                    $totaisPorTipo[3] += (int)$r['QTD_CRED'];
                    $totDescReal      += (float)$r['VAL_DESC_REAL'];
                    $totAutorizado    += (float)$r['VAL_AUTORIZADO'];
                    $totQtdDescGeral  += (int)$r['QTD_DESC'];
                }
                $mediaPorOper       = count($rowsS) > 0 ? ($totalGeral / count($rowsS)) : 0;
                // Ticket Desconto medio da loja: so faz sentido em cima de TIPO=1
                $ticketMedioLoja    = $totQtdDescGeral > 0 ? ($totDescReal / $totQtdDescGeral) : 0.0;
                $qtdOperAtivos      = count($rowsS);
                $qtdSupAtivos       = count($rowsSup);

                // P90 do Desconto R$ entre caixas — usado pra calibrar ALERTA
                $descReaisArr = [];
                foreach ($rowsS as $r) {
                    $v = (float)$r['VAL_DESC_REAL'];
                    if ($v > 0) $descReaisArr[] = $v;
                }
                sort($descReaisArr);
                $nP90 = count($descReaisArr);
                $p90DescCaixa = 0.0;
                if ($nP90 > 0) {
                    $idxP90 = (int)floor(0.9 * ($nP90 - 1));
                    $p90DescCaixa = $descReaisArr[$idxP90];
                }

                // helper faixa horaria
                $faixaPredominante = function(array $r): array {
                    $faixas = [
                        'madrugada' => (int)($r['QTD_MADRUGADA'] ?? 0),
                        'manha'     => (int)($r['QTD_MANHA']     ?? 0),
                        'tarde'     => (int)($r['QTD_TARDE']     ?? 0),
                        'noite'     => (int)($r['QTD_NOITE']     ?? 0),
                    ];
                    arsort($faixas);
                    $top = array_key_first($faixas);
                    $qtdTop = $faixas[$top];
                    $totFaixas = array_sum($faixas);
                    $pct = $totFaixas > 0 ? round($qtdTop / $totFaixas * 100, 1) : 0.0;
                    return ['faixa' => $top, 'qtd' => $qtdTop, 'pct' => $pct, 'distrib' => $faixas];
                };

                // ── SOLICITANTES enriquecidos ────────────────────────────────
                $solicitantes = [];
                foreach ($rowsS as $r) {
                    $nome = $u(trim((string)$r['SOLICITANTE']));
                    $qtd  = (int)$r['QTD_TOTAL'];
                    $qtdDesc      = (int)$r['QTD_DESC'];
                    $valDescReal  = round((float)$r['VAL_DESC_REAL'], 2);   // dinheiro real (TIPO=1)
                    $valAutorizado= round((float)$r['VAL_AUTORIZADO'], 2);  // fatura/limite (TIPO 2,3)
                    // Ticket Desconto honesto: SUM(DESCONTO TIPO=1) / COUNT(TIPO=1)
                    // null sinaliza pro front exibir "-" (caixa so faz pagamento, sem componente monetario)
                    $ticket = $qtdDesc > 0 ? round($valDescReal / $qtdDesc, 2) : null;
                    $pctLoja = $totalGeral > 0 ? round($qtd / $totalGeral * 100, 1) : 0.0;

                    // Supervisor preferido — concentracao calculada SO em cima de TIPO=1
                    // (faz mais sentido como sinal de fraude: quem libera o desconto, nao o pagamento)
                    $supTopNome = ''; $supTopQ = 0; $supTopPct = 0.0;
                    if (isset($pivotPorOp[$nome])) {
                        $arr = $pivotPorOp[$nome];
                        arsort($arr);
                        $supTopNome = (string)array_key_first($arr);
                        $supTopQ    = (int)$arr[$supTopNome];
                        $supTopPct  = $qtd > 0 ? round($supTopQ / $qtd * 100, 1) : 0.0;
                    }

                    $faixa = $faixaPredominante($r);

                    // Flag conluio v3: ALERTA dispara em cima de Desconto R$ (TIPO=1 only),
                    // nao mais sobre SUM(DESCONTO) total inflado.
                    // Criterios:
                    //  (a) Desconto R$ do caixa > P90 dos caixas no periodo
                    //  (b) Concentracao no supervisor preferido > 70% das liberacoes
                    //  (c) Ticket Desconto > 2x media da loja
                    // Autorizado R$ NAO entra (e limite/fatura, nao fraude potencial)
                    $alertaConluio = false;
                    $motivosAlerta = [];
                    if ($supTopPct >= 70 && $qtd >= 5 && $qtdSupAtivos >= 2) {
                        $alertaConluio = true;
                        $motivosAlerta[] = $supTopPct . '% das liberações deste caixa foram autorizadas pelo mesmo supervisor';
                    }
                    if ($p90DescCaixa > 0 && $valDescReal > $p90DescCaixa && $qtdDesc >= 3) {
                        $alertaConluio = true;
                        $motivosAlerta[] = 'Desconto R$ deste caixa está acima do P90 do período (R$ '
                                         . number_format($valDescReal, 2, ',', '.') . ')';
                    }
                    if ($ticketMedioLoja > 0 && $ticket !== null && $ticket > 2 * $ticketMedioLoja && $qtdDesc >= 3) {
                        $alertaConluio = true;
                        $motivosAlerta[] = 'Ticket Desconto deste caixa está 2x acima da média da loja';
                    }

                    $solicitantes[] = [
                        'solicitante'        => $nome,
                        'qtd_total'          => $qtd,
                        'qtd_pgto'           => (int)$r['QTD_PGTO'],
                        'qtd_desconto'       => $qtdDesc,
                        'qtd_fatura'         => (int)$r['QTD_FATURA'],
                        'qtd_credito'        => (int)$r['QTD_CRED'],
                        // v3: chaves separadas; "val_desconto" legado mantido como apelido p/ compat
                        'val_desconto'       => $valDescReal,
                        'val_desconto_real'  => $valDescReal,
                        'val_autorizado'     => $valAutorizado,
                        'val_total'          => $valDescReal, // legado p/ telas antigas — agora aponta pra desconto real
                        'val_permitido'      => round((float)$r['VAL_PERMIT_SOLIC'], 2),
                        'ticket_desconto'    => $ticket,           // pode ser null (sem TIPO=1)
                        'ticket_medio'       => $ticket ?? 0.0,    // legado
                        'pct_loja'           => $pctLoja,
                        'supervisor_top'     => $supTopNome,
                        'supervisor_top_qtd' => $supTopQ,
                        'supervisor_top_pct' => $supTopPct,
                        'faixa'              => $faixa['faixa'],
                        'faixa_pct'          => $faixa['pct'],
                        'faixa_distrib'      => $faixa['distrib'],
                        'acima_media'        => ($mediaPorOper > 0 && $qtd > $mediaPorOper * 2),
                        'alerta_conluio'     => $alertaConluio,
                        'motivos_alerta'     => $motivosAlerta,
                    ];
                }

                // ── SUPERVISORES enriquecidos ────────────────────────────────
                // v3: ticket medio sup loja agora calculado em DESCONTO R$ real / QTD DESC, nao no total inflado
                $totDescSup = 0.0; $totQtdDescSup = 0;
                foreach ($rowsSup as $r) {
                    $totDescSup    += (float)$r['VAL_DESC_REAL'];
                    $totQtdDescSup += (int)$r['QTD_DESC'];
                }
                $ticketMedioSupLoja = $totQtdDescSup > 0 ? ($totDescSup / $totQtdDescSup) : 0.0;

                // P90 do Desconto R$ entre supervisores
                $descReaisArrSup = [];
                foreach ($rowsSup as $r) {
                    $v = (float)$r['VAL_DESC_REAL'];
                    if ($v > 0) $descReaisArrSup[] = $v;
                }
                sort($descReaisArrSup);
                $nSupP90 = count($descReaisArrSup);
                $p90DescSup = 0.0;
                if ($nSupP90 > 0) {
                    $idxP90 = (int)floor(0.9 * ($nSupP90 - 1));
                    $p90DescSup = $descReaisArrSup[$idxP90];
                }

                $supervisores = [];
                foreach ($rowsSup as $r) {
                    $nome   = $u(trim((string)$r['SUPERVISOR']));
                    $qtd    = (int)$r['QTD'];
                    $qtdDesc      = (int)$r['QTD_DESC'];
                    $valDescReal  = round((float)$r['VAL_DESC_REAL'], 2);
                    $valAutorizado= round((float)$r['VAL_AUTORIZADO'], 2);
                    $valPer = round((float)$r['VAL_PERMIT'], 2);
                    $spread = round((float)$r['SPREAD_AUTORIZADO'], 2);
                    // Ticket Desconto honesto: SUM(DESCONTO TIPO=1) / COUNT(TIPO=1)
                    $ticket = $qtdDesc > 0 ? round($valDescReal / $qtdDesc, 2) : null;
                    $pctLoja = $totalGeral > 0 ? round($qtd / $totalGeral * 100, 1) : 0.0;

                    // Operador preferido
                    $opTopNome = ''; $opTopQ = 0; $opTopPct = 0.0;
                    if (isset($pivotPorSup[$nome])) {
                        $arr = $pivotPorSup[$nome];
                        arsort($arr);
                        $opTopNome = (string)array_key_first($arr);
                        $opTopQ    = (int)$arr[$opTopNome];
                        $opTopPct  = $qtd > 0 ? round($opTopQ / $qtd * 100, 1) : 0.0;
                    }

                    $faixa = $faixaPredominante($r);

                    // ALERTA v3: criterios baseados em Desconto R$ (TIPO=1 only)
                    $alertaConluio = false;
                    $motivosAlerta = [];
                    if ($opTopPct >= 70 && $qtd >= 5 && $qtdOperAtivos >= 2) {
                        $alertaConluio = true;
                        $motivosAlerta[] = $opTopPct . '% das autorizações deste supervisor foram para o mesmo caixa';
                    }
                    if ($p90DescSup > 0 && $valDescReal > $p90DescSup && $qtdDesc >= 3) {
                        $alertaConluio = true;
                        $motivosAlerta[] = 'Desconto R$ deste supervisor está acima do P90 do período (R$ '
                                         . number_format($valDescReal, 2, ',', '.') . ')';
                    }
                    if ($spread > 1000 && $qtdDesc >= 3) {
                        $alertaConluio = true;
                        $motivosAlerta[] = 'Supervisor autorizou R$ ' . number_format($spread, 2, ',', '.') . ' de desconto acima do limite permitido';
                    }
                    if ($faixa['pct'] >= 80 && $qtd >= 5) {
                        $alertaConluio = true;
                        $motivosAlerta[] = $faixa['pct'] . '% das autorizações foram concentradas em um único período do dia';
                    }

                    $supervisores[] = [
                        'supervisor'         => $nome,
                        'qtd'                => $qtd,
                        'qtd_desconto'       => $qtdDesc,
                        // v3: separa em Desconto R$ (TIPO=1) e Autorizado R$ (TIPO 2,3)
                        'val_desconto_real'  => $valDescReal,
                        'val_autorizado'     => $valAutorizado,
                        // legado: val_autorizado antes era a soma total — mantemos o nome
                        // mas agora aponta SO pra Desconto R$. Quem quiser fatura/limite usa val_autorizado_credito.
                        'val_autorizado_credito' => $valAutorizado,
                        'val_permitido'      => $valPer,
                        'spread'             => $spread,
                        'ticket_desconto'    => $ticket,
                        'ticket_medio'       => $ticket ?? 0.0, // legado
                        'pct_loja'           => $pctLoja,
                        'operador_top'       => $opTopNome,
                        'operador_top_qtd'   => $opTopQ,
                        'operador_top_pct'   => $opTopPct,
                        'faixa'              => $faixa['faixa'],
                        'faixa_pct'          => $faixa['pct'],
                        'faixa_distrib'      => $faixa['distrib'],
                        'alerta_conluio'     => $alertaConluio,
                        'motivos_alerta'     => $motivosAlerta,
                    ];
                }

                return [
                    'solicitantes'        => $solicitantes,
                    'supervisores'        => $supervisores,
                    'total_geral'         => $totalGeral,
                    'totais_por_tipo'     => $totaisPorTipo,
                    'media_por_oper'      => round($mediaPorOper, 1),
                    // v3: split do total geral em Desconto R$ (TIPO=1) x Autorizado R$ (TIPO 2,3)
                    'valor_desconto_real' => round($totDescReal, 2),
                    'valor_autorizado'    => round($totAutorizado, 2),
                    'valor_total_geral'   => round($totDescReal, 2), // legado — agora aponta pra Desconto R$
                    'ticket_medio_loja'   => round($ticketMedioLoja, 2),
                    'ticket_medio_sup'    => round($ticketMedioSupLoja, 2),
                ];
            } catch (\Throwable $e) {
                return ['solicitantes' => [], 'supervisores' => [], 'total_geral' => 0,
                        'totais_por_tipo' => [0=>0,1=>0,2=>0,3=>0], 'media_por_oper' => 0,
                        'valor_desconto_real' => 0, 'valor_autorizado' => 0,
                        'valor_total_geral' => 0, 'ticket_medio_loja' => 0, 'ticket_medio_sup' => 0,
                        'erro' => $e->getMessage()];
            }
        });
    }

    /**
     * Detalhe completo de UMA liberação: cliente, situação financeira,
     * histórico de liberações desse cliente, e nota gerada após.
     */
    public function detalheLiberacao(int $codSeq): array
    {
        $emp = $this->filtroEmpresa('l');
        try {
            $u = fn($s) => ($s !== null && !mb_check_encoding((string)$s, 'UTF-8'))
                ? mb_convert_encoding((string)$s, 'UTF-8', 'Windows-1252') : (string)$s;

            // 1) A liberação em si
            $sqlL = "
                SELECT l.COD_SEQ, l.COD_EMPRESA, l.COD_PESSOA, l.DATA, l.HORA,
                       l.TIPO_LIBERACAO, l.ACESSO, l.STATUS,
                       TRIM(l.COD_USUARIO_SOLICITANTE) AS SOLICITANTE,
                       TRIM(l.COD_USUARIO_LIBEROU)     AS SUPERVISOR,
                       TRIM(l.MENSAGEM) AS MENSAGEM, TRIM(l.OBS) AS OBS,
                       CAST(l.DESCONTO AS DOUBLE PRECISION) AS VALOR_SOLIC,
                       CAST(l.DESCPERM AS DOUBLE PRECISION) AS VALOR_PERMIT
                FROM LIBERACOES l
                WHERE l.COD_SEQ = {$codSeq}
                  {$emp}
            ";
            $lib = $this->pdo->query($sqlL)->fetch(\PDO::FETCH_ASSOC);
            if (!$lib) return ['ok' => false, 'erro' => 'Liberação não encontrada.'];

            $codPessoa = (int)$lib['COD_PESSOA'];
            $codEmp    = (int)$lib['COD_EMPRESA'];
            $data      = (string)$lib['DATA'];

            // 2) Cliente (se houver COD_PESSOA)
            $cliente = null;
            if ($codPessoa > 0) {
                try {
                    $sqlC = "
                        SELECT p.COD_PESSOA, TRIM(p.NOME) AS NOME,
                               p.CPF, p.CNPJ, TRIM(p.FONE_PRINC) AS FONE, TRIM(p.CEL_PRINC) AS CEL,
                               CAST(p.LIMITE_CREDITO AS DOUBLE PRECISION) AS LIMITE
                        FROM PESSOAS p
                        WHERE p.COD_PESSOA = {$codPessoa}
                    ";
                    $cr = $this->pdo->query($sqlC)->fetch(\PDO::FETCH_ASSOC);
                    if ($cr) {
                        $cliente = [
                            'cod_pessoa' => (int)$cr['COD_PESSOA'],
                            'nome'       => $u(trim((string)$cr['NOME'])),
                            'cpf'        => trim((string)($cr['CPF']  ?? '')),
                            'cnpj'       => trim((string)($cr['CNPJ'] ?? '')),
                            'fone'       => $u(trim((string)($cr['FONE'] ?? ''))),
                            'cel'        => $u(trim((string)($cr['CEL']  ?? ''))),
                            'limite'     => round((float)($cr['LIMITE'] ?? 0), 2),
                        ];
                    }
                } catch (\Throwable) {}
            }

            // 3) Situação financeira do cliente
            $financeiro = null;
            if ($codPessoa > 0) {
                try {
                    $sqlF = "
                        SELECT COUNT(*)                                                                    AS QTD,
                               COALESCE(SUM(CAST(SALDO_TITULO AS DOUBLE PRECISION)), 0)                    AS SALDO,
                               COALESCE(SUM(CASE WHEN VENCIMENTO < CURRENT_DATE
                                                 THEN CAST(SALDO_TITULO AS DOUBLE PRECISION) ELSE 0 END),0) AS VENCIDO,
                               MIN(CASE WHEN VENCIMENTO < CURRENT_DATE THEN VENCIMENTO END)                AS VENC_MAIS_ANTIGO
                        FROM RECEBER_PAGAR
                        WHERE COD_PESSOA = {$codPessoa} AND COD_EMPRESA = {$codEmp} AND SALDO_TITULO > 0
                    ";
                    $fr = $this->pdo->query($sqlF)->fetch(\PDO::FETCH_ASSOC);
                    $vMaisAntigo = $fr['VENC_MAIS_ANTIGO'] ?? null;
                    $diasAtraso = 0;
                    if ($vMaisAntigo) {
                        $dt = strtotime((string)$vMaisAntigo);
                        if ($dt) $diasAtraso = max(0, (int)((time() - $dt) / 86400));
                    }
                    $financeiro = [
                        'qtd_titulos'    => (int)$fr['QTD'],
                        'saldo_total'    => round((float)$fr['SALDO'], 2),
                        'saldo_vencido'  => round((float)$fr['VENCIDO'], 2),
                        'dias_atraso'    => $diasAtraso,
                    ];
                } catch (\Throwable) {}
            }

            // 4) Histórico de liberações desse cliente
            $historico = null;
            if ($codPessoa > 0) {
                try {
                    $sqlH = "
                        SELECT COUNT(*) Q,
                               COUNT(CASE WHEN TIPO_LIBERACAO=0 THEN 1 END) Q0,
                               COUNT(CASE WHEN TIPO_LIBERACAO=1 THEN 1 END) Q1,
                               COUNT(CASE WHEN TIPO_LIBERACAO=2 THEN 1 END) Q2,
                               COUNT(CASE WHEN TIPO_LIBERACAO=3 THEN 1 END) Q3
                        FROM LIBERACOES
                        WHERE COD_PESSOA = {$codPessoa} AND COD_EMPRESA = {$codEmp}
                    ";
                    $hr = $this->pdo->query($sqlH)->fetch(\PDO::FETCH_ASSOC);
                    $historico = [
                        'total'       => (int)$hr['Q'],
                        'por_tipo'    => [
                            0 => (int)$hr['Q0'],
                            1 => (int)$hr['Q1'],
                            2 => (int)$hr['Q2'],
                            3 => (int)$hr['Q3'],
                        ],
                    ];
                } catch (\Throwable) {}
            }

            // 5) Notas geradas no MESMO DIA pra esse cliente (provavelmente a venda resultante)
            $notas = [];
            if ($codPessoa > 0) {
                try {
                    $sqlN = "
                        SELECT FIRST 5
                            nc.NR_NOTA, TRIM(nc.SERIE) AS SERIE,
                            nc.DATA_EMISSAO, nc.DATA_CANC,
                            CAST(nc.TOTAL_NOTA AS DOUBLE PRECISION) AS TOTAL_NOTA,
                            CAST(nc.DESCONTO   AS DOUBLE PRECISION) AS DESCONTO,
                            nc.COD_TIPO_OPERACAO,
                            TRIM(op.DESCRICAO) AS OPERACAO
                        FROM NOTAS_CAB nc
                        LEFT JOIN TIPO_OPERACAO op ON op.COD_TIPO_OPERACAO=nc.COD_TIPO_OPERACAO AND op.COD_EMPRESA=nc.COD_EMPRESA
                        WHERE nc.COD_EMPRESA = {$codEmp}
                          AND nc.COD_PESSOA = {$codPessoa}
                          AND nc.DATA_EMISSAO = '{$data}'
                        ORDER BY nc.NR_NOTA DESC
                    ";
                    foreach ($this->pdo->query($sqlN)->fetchAll(\PDO::FETCH_ASSOC) as $n) {
                        $notas[] = [
                            'nr_nota'    => (int)$n['NR_NOTA'],
                            'serie'      => trim((string)$n['SERIE']),
                            'data'       => (string)$n['DATA_EMISSAO'],
                            'data_canc'  => (string)($n['DATA_CANC'] ?? ''),
                            'total'      => round((float)$n['TOTAL_NOTA'], 2),
                            'desconto'   => round((float)$n['DESCONTO'], 2),
                            'op_cod'     => (int)$n['COD_TIPO_OPERACAO'],
                            'op_descr'   => $u(trim((string)($n['OPERACAO'] ?? ''))),
                            'cancelada'  => !empty($n['DATA_CANC']),
                        ];
                    }
                } catch (\Throwable) {}
            }

            return [
                'ok' => true,
                'liberacao' => [
                    'cod_seq'      => (int)$lib['COD_SEQ'],
                    'data'         => (string)$lib['DATA'],
                    'hora'         => (string)$lib['HORA'],
                    'tipo'         => (int)$lib['TIPO_LIBERACAO'],
                    'acesso'       => (string)$lib['ACESSO'],
                    'status'       => (string)$lib['STATUS'],
                    'solicitante'  => $u(trim((string)($lib['SOLICITANTE'] ?? ''))),
                    'supervisor'   => $u(trim((string)($lib['SUPERVISOR'] ?? ''))),
                    'mensagem'     => $u(trim((string)($lib['MENSAGEM'] ?? ''))),
                    'obs'          => $u(trim((string)($lib['OBS'] ?? ''))),
                    'valor_solic'  => round((float)$lib['VALOR_SOLIC'], 2),
                    'valor_permit' => round((float)$lib['VALOR_PERMIT'], 2),
                ],
                'cliente'    => $cliente,
                'financeiro' => $financeiro,
                'historico'  => $historico,
                'notas'      => $notas,
            ];
        } catch (\Throwable $e) {
            return ['ok' => false, 'erro' => $e->getMessage()];
        }
    }

    /**
     * Drill-down: liberações detalhadas de UM solicitante no período.
     */
    public function liberacoesDoSolicitante(string $dataIni, string $dataFim, string $solicitante, ?int $tipo = null): array
    {
        $emp = $this->filtroEmpresa('l');
        $solEsc  = str_replace("'", "''", trim($solicitante));
        $tipoSql = $tipo !== null ? "AND l.TIPO_LIBERACAO = " . (int)$tipo : '';
        try {
            // v1.4.34: LEFT JOIN agregado em NOTAS_CAB (chave heuristica
            // COD_EMPRESA + COD_PESSOA + DATA_EMISSAO=l.DATA) — pode dar 0..N
            // notas por liberacao no mesmo dia, entao agregamos numa subquery
            // para nao multiplicar linhas.
            $sql = "
                SELECT FIRST 500
                    l.COD_SEQ,
                    l.DATA, l.HORA,
                    l.TIPO_LIBERACAO,
                    TRIM(l.COD_USUARIO_LIBEROU)   AS SUPERVISOR,
                    TRIM(l.MENSAGEM)              AS MENSAGEM,
                    TRIM(l.OBS)                   AS OBS,
                    CAST(l.DESCONTO AS DOUBLE PRECISION) AS VALOR_SOLIC,
                    CAST(l.DESCPERM AS DOUBLE PRECISION) AS VALOR_PERMIT,
                    l.ACESSO,
                    l.STATUS,
                    l.COD_PESSOA,
                    TRIM(p.NOME) AS CLIENTE_NOME,
                    nc.VAL_NOTA_DIA          AS VALOR_NOTA,
                    nc.NR_NOTA_PRINCIPAL     AS NR_NOTA,
                    nc.SERIE_PRINCIPAL       AS SERIE_NOTA,
                    nc.QTD_NOTAS_DIA         AS QTD_NOTAS
                FROM LIBERACOES l
                LEFT JOIN PESSOAS p ON p.COD_PESSOA = l.COD_PESSOA
                LEFT JOIN (
                    SELECT
                        nc2.COD_EMPRESA  AS COD_EMP,
                        nc2.COD_PESSOA   AS COD_PES,
                        nc2.DATA_EMISSAO AS DT_EMI,
                        SUM(CAST(nc2.TOTAL_NOTA AS DOUBLE PRECISION)) AS VAL_NOTA_DIA,
                        MAX(nc2.NR_NOTA) AS NR_NOTA_PRINCIPAL,
                        MAX(TRIM(nc2.SERIE))  AS SERIE_PRINCIPAL,
                        COUNT(*)         AS QTD_NOTAS_DIA
                    FROM NOTAS_CAB nc2
                    WHERE nc2.DATA_EMISSAO BETWEEN '{$dataIni}' AND '{$dataFim}'
                      AND nc2.DATA_CANC IS NULL
                    GROUP BY nc2.COD_EMPRESA, nc2.COD_PESSOA, nc2.DATA_EMISSAO
                ) nc ON nc.COD_EMP = l.COD_EMPRESA
                    AND nc.COD_PES = l.COD_PESSOA
                    AND nc.DT_EMI  = l.DATA
                WHERE l.DATA BETWEEN '{$dataIni}' AND '{$dataFim}'
                  AND TRIM(l.COD_USUARIO_SOLICITANTE) = '{$solEsc}'
                  {$emp}
                  {$tipoSql}
                ORDER BY l.DATA DESC, l.HORA DESC
            ";
            $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
            $out = [];
            $u = fn($s) => ($s !== null && !mb_check_encoding((string)$s, 'UTF-8'))
                ? mb_convert_encoding((string)$s, 'UTF-8', 'Windows-1252') : (string)$s;
            foreach ($rows as $r) {
                $valS    = round((float)$r['VALOR_SOLIC'], 2);
                $valP    = round((float)$r['VALOR_PERMIT'], 2);
                $valNota = isset($r['VALOR_NOTA']) ? round((float)$r['VALOR_NOTA'], 2) : 0.0;
                $pctDesc = $valNota > 0 ? round($valS / $valNota * 100, 2) : null;
                $out[] = [
                    'cod_seq'      => (int)$r['COD_SEQ'],
                    'data'         => (string)$r['DATA'],
                    'hora'         => (string)$r['HORA'],
                    'tipo'         => (int)$r['TIPO_LIBERACAO'],
                    'supervisor'   => $u(trim((string)($r['SUPERVISOR'] ?? ''))),
                    'mensagem'     => $u(trim((string)($r['MENSAGEM'] ?? ''))),
                    'obs'          => $u(trim((string)($r['OBS'] ?? ''))),
                    'valor_solic'  => $valS,
                    'valor_permit' => $valP,
                    'spread'       => round($valS - $valP, 2),
                    'acesso'       => trim((string)($r['ACESSO'] ?? '')),
                    'status'       => trim((string)($r['STATUS'] ?? '')),
                    'cod_pessoa'   => (int)($r['COD_PESSOA'] ?? 0),
                    'cliente_nome' => $u(trim((string)($r['CLIENTE_NOME'] ?? ''))),
                    'valor_nota'   => $valNota,
                    'pct_desc'     => $pctDesc,
                    'nr_nota'      => (int)($r['NR_NOTA'] ?? 0),
                    'serie_nota'   => trim((string)($r['SERIE_NOTA'] ?? '')),
                    'qtd_notas'    => (int)($r['QTD_NOTAS'] ?? 0),
                ];
            }
            return $out;
        } catch (\Throwable) {
            return [];
        }
    }

    /**
     * Drill-down: liberacoes detalhadas autorizadas por UM supervisor no periodo.
     * Espelho do liberacoesDoSolicitante, troca COD_USUARIO_SOLICITANTE por COD_USUARIO_LIBEROU.
     */
    public function liberacoesPorSupervisor(string $dataIni, string $dataFim, string $supervisor, ?int $tipo = null): array
    {
        $emp = $this->filtroEmpresa('l');
        $supEsc  = str_replace("'", "''", trim($supervisor));
        $tipoSql = $tipo !== null ? "AND l.TIPO_LIBERACAO = " . (int)$tipo : '';
        try {
            // v1.4.34: LEFT JOIN agregado em NOTAS_CAB — ver liberacoesDoSolicitante.
            $sql = "
                SELECT FIRST 500
                    l.COD_SEQ,
                    l.DATA, l.HORA,
                    l.TIPO_LIBERACAO,
                    TRIM(l.COD_USUARIO_SOLICITANTE) AS OPERADOR,
                    TRIM(l.MENSAGEM)                AS MENSAGEM,
                    TRIM(l.OBS)                     AS OBS,
                    CAST(l.DESCONTO AS DOUBLE PRECISION) AS VALOR_SOLIC,
                    CAST(l.DESCPERM AS DOUBLE PRECISION) AS VALOR_PERMIT,
                    l.ACESSO,
                    l.STATUS,
                    l.COD_PESSOA,
                    TRIM(p.NOME) AS CLIENTE_NOME,
                    nc.VAL_NOTA_DIA          AS VALOR_NOTA,
                    nc.NR_NOTA_PRINCIPAL     AS NR_NOTA,
                    nc.SERIE_PRINCIPAL       AS SERIE_NOTA,
                    nc.QTD_NOTAS_DIA         AS QTD_NOTAS
                FROM LIBERACOES l
                LEFT JOIN PESSOAS p ON p.COD_PESSOA = l.COD_PESSOA
                LEFT JOIN (
                    SELECT
                        nc2.COD_EMPRESA  AS COD_EMP,
                        nc2.COD_PESSOA   AS COD_PES,
                        nc2.DATA_EMISSAO AS DT_EMI,
                        SUM(CAST(nc2.TOTAL_NOTA AS DOUBLE PRECISION)) AS VAL_NOTA_DIA,
                        MAX(nc2.NR_NOTA) AS NR_NOTA_PRINCIPAL,
                        MAX(TRIM(nc2.SERIE))  AS SERIE_PRINCIPAL,
                        COUNT(*)         AS QTD_NOTAS_DIA
                    FROM NOTAS_CAB nc2
                    WHERE nc2.DATA_EMISSAO BETWEEN '{$dataIni}' AND '{$dataFim}'
                      AND nc2.DATA_CANC IS NULL
                    GROUP BY nc2.COD_EMPRESA, nc2.COD_PESSOA, nc2.DATA_EMISSAO
                ) nc ON nc.COD_EMP = l.COD_EMPRESA
                    AND nc.COD_PES = l.COD_PESSOA
                    AND nc.DT_EMI  = l.DATA
                WHERE l.DATA BETWEEN '{$dataIni}' AND '{$dataFim}'
                  AND TRIM(l.COD_USUARIO_LIBEROU) = '{$supEsc}'
                  {$emp}
                  {$tipoSql}
                ORDER BY l.DATA DESC, l.HORA DESC
            ";
            $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
            $out = [];
            $u = fn($s) => ($s !== null && !mb_check_encoding((string)$s, 'UTF-8'))
                ? mb_convert_encoding((string)$s, 'UTF-8', 'Windows-1252') : (string)$s;
            foreach ($rows as $r) {
                $valS    = round((float)$r['VALOR_SOLIC'], 2);
                $valP    = round((float)$r['VALOR_PERMIT'], 2);
                $valNota = isset($r['VALOR_NOTA']) ? round((float)$r['VALOR_NOTA'], 2) : 0.0;
                $pctDesc = $valNota > 0 ? round($valS / $valNota * 100, 2) : null;
                $out[] = [
                    'cod_seq'      => (int)$r['COD_SEQ'],
                    'data'         => (string)$r['DATA'],
                    'hora'         => (string)$r['HORA'],
                    'tipo'         => (int)$r['TIPO_LIBERACAO'],
                    'operador'     => $u(trim((string)($r['OPERADOR'] ?? ''))),
                    'mensagem'     => $u(trim((string)($r['MENSAGEM'] ?? ''))),
                    'obs'          => $u(trim((string)($r['OBS'] ?? ''))),
                    'valor_solic'  => $valS,
                    'valor_permit' => $valP,
                    'spread'       => round($valS - $valP, 2),
                    'acesso'       => trim((string)($r['ACESSO'] ?? '')),
                    'status'       => trim((string)($r['STATUS'] ?? '')),
                    'cod_pessoa'   => (int)($r['COD_PESSOA'] ?? 0),
                    'cliente_nome' => $u(trim((string)($r['CLIENTE_NOME'] ?? ''))),
                    'valor_nota'   => $valNota,
                    'pct_desc'     => $pctDesc,
                    'nr_nota'      => (int)($r['NR_NOTA'] ?? 0),
                    'serie_nota'   => trim((string)($r['SERIE_NOTA'] ?? '')),
                    'qtd_notas'    => (int)($r['QTD_NOTAS'] ?? 0),
                ];
            }
            return $out;
        } catch (\Throwable) {
            return [];
        }
    }

    // -------------------------------------------------------------------------
    // CANCELAMENTOS POR OPERADORA
    // -------------------------------------------------------------------------

    /**
     * Cancelamentos por operadora no período (DATA_CANC entre as datas).
     * Compara com vendas válidas do MESMO período pra calcular % cancelamento.
     */
    public function cancelamentosPorOperadora(string $dataIni, string $dataFim): array
    {
        // v2: agrupa por NOME (COALESCE da operadora de caixa OU do USUARIO_CANC).
        // Antes usava INNER JOIN com ABERTURA_CAIXA e perdia ~50% dos cancelamentos
        // (os "administrativos" feitos via NFe não passam por sessão de caixa).
        $emp      = $this->filtroEmpresa('nc');
        $cacheKey = "audit_canc_oper_v2_{$dataIni}_{$dataFim}" . $this->escopoCache();

        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($dataIni, $dataFim, $emp) {
            try {
                $sql = "
                    SELECT
                        UPPER(TRIM(COALESCE(u.NOME, nc.USUARIO_CANC, '(desconhecido)'))) AS OPERADORA,
                        COUNT(CASE WHEN nc.DATA_CANC BETWEEN '{$dataIni}' AND '{$dataFim}'
                              THEN 1 END)                                 AS QTD_CANCELADAS,
                        COALESCE(SUM(CASE WHEN nc.DATA_CANC BETWEEN '{$dataIni}' AND '{$dataFim}'
                              THEN CAST(nc.TOTAL_NOTA AS DOUBLE PRECISION) ELSE 0 END), 0) AS VALOR_CANC,
                        COUNT(CASE WHEN nc.DATA_CANC BETWEEN '{$dataIni}' AND '{$dataFim}'
                              AND nc.COD_ABERT_CAIXA IS NOT NULL THEN 1 END) AS QTD_CAIXA,
                        COUNT(CASE WHEN nc.DATA_CANC BETWEEN '{$dataIni}' AND '{$dataFim}'
                              AND nc.COD_ABERT_CAIXA IS NULL THEN 1 END)     AS QTD_ADMIN,
                        COUNT(CASE WHEN nc.DATA_CANC IS NULL
                              AND nc.DATA_EMISSAO BETWEEN '{$dataIni}' AND '{$dataFim}'
                              THEN 1 END)                                 AS QTD_VALIDAS,
                        COALESCE(SUM(CASE WHEN nc.DATA_CANC IS NULL
                              AND nc.DATA_EMISSAO BETWEEN '{$dataIni}' AND '{$dataFim}'
                              THEN CAST(nc.TOTAL_NOTA AS DOUBLE PRECISION) ELSE 0 END), 0) AS VALOR_VALIDO
                    FROM NOTAS_CAB nc
                    LEFT JOIN ABERTURA_CAIXA ac ON ac.COD_ABERT_CAIXA = nc.COD_ABERT_CAIXA
                    LEFT JOIN USUARIOS u        ON u.COD_USUARIO     = ac.COD_USUARIO_ABERTURA
                    WHERE ((nc.DATA_CANC BETWEEN '{$dataIni}' AND '{$dataFim}')
                        OR (nc.DATA_EMISSAO BETWEEN '{$dataIni}' AND '{$dataFim}' AND nc.DATA_CANC IS NULL))
                      {$emp}
                    GROUP BY UPPER(TRIM(COALESCE(u.NOME, nc.USUARIO_CANC, '(desconhecido)')))
                    HAVING COUNT(CASE WHEN nc.DATA_CANC BETWEEN '{$dataIni}' AND '{$dataFim}' THEN 1 END) > 0
                    ORDER BY VALOR_CANC DESC
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);

                $totCanc = 0.0; $totVal = 0.0;
                foreach ($rows as $r) {
                    $totCanc += (float)$r['VALOR_CANC'];
                    $totVal  += (float)$r['VALOR_VALIDO'];
                }
                $mediaPct = $totVal > 0 ? ($totCanc / $totVal * 100) : 0;

                $out = [];
                foreach ($rows as $r) {
                    $vc    = (float)$r['VALOR_CANC'];
                    $vv    = (float)$r['VALOR_VALIDO'];
                    $qc    = (int)$r['QTD_CANCELADAS'];
                    $qv    = (int)$r['QTD_VALIDAS'];
                    $qCx   = (int)$r['QTD_CAIXA'];
                    $qAd   = (int)$r['QTD_ADMIN'];
                    $pct   = $vv > 0 ? ($vc / $vv * 100) : 0;
                    $nome  = (string)$r['OPERADORA'];
                    if (!mb_check_encoding($nome, 'UTF-8')) {
                        $nome = mb_convert_encoding($nome, 'UTF-8', 'Windows-1252');
                    }
                    // origem dominante: se a maioria veio via caixa => 'Caixa', senão 'Administrativo', se mistura => 'Misto'
                    $origem = $qCx > 0 && $qAd > 0 ? 'Misto'
                            : ($qCx > 0 ? 'Caixa' : 'Administrativo');
                    $out[] = [
                        'operadora'        => $nome,
                        'qtd_canceladas'   => $qc,
                        'qtd_caixa'        => $qCx,
                        'qtd_admin'        => $qAd,
                        'origem'           => $origem,
                        'valor_cancelado'  => round($vc, 2),
                        'qtd_validas'      => $qv,
                        'valor_valido'     => round($vv, 2),
                        'pct_cancelamento' => round($pct, 4),
                        'acima_media'      => ($mediaPct > 0 && $pct > $mediaPct),
                    ];
                }

                return [
                    'lista'      => $out,
                    'total_canc' => round($totCanc, 2),
                    'total_val'  => round($totVal, 2),
                    'qtd_canc'   => array_sum(array_column($out, 'qtd_canceladas')),
                    'media_pct'  => round($mediaPct, 4),
                ];
            } catch (\Throwable $e) {
                return ['lista' => [], 'total_canc' => 0.0, 'total_val' => 0.0, 'qtd_canc' => 0, 'media_pct' => 0.0, 'erro' => $e->getMessage()];
            }
        });
    }

    /**
     * Lista as notas CANCELADAS de uma operadora no período (drill-down).
     * v2: busca por NOME (compatível com cancelamento via caixa OU administrativo).
     */
    public function notasCanceladasDaOperadora(string $dataIni, string $dataFim, string $nomeOperadora): array
    {
        $emp = $this->filtroEmpresa('nc');
        try {
            $nomeEsc = str_replace("'", "''", strtoupper(trim($nomeOperadora)));
            $sql = "
                SELECT FIRST 500
                    nc.NR_NOTA, TRIM(nc.SERIE) AS SERIE,
                    nc.DATA_EMISSAO, nc.DATA_CANC,
                    CAST(nc.TOTAL_NOTA AS DOUBLE PRECISION) AS TOTAL_NOTA,
                    nc.COD_PESSOA, TRIM(p.NOME) AS CLIENTE,
                    TRIM(nc.HISTORICO_CANC) AS MOTIVO,
                    CASE WHEN nc.COD_ABERT_CAIXA IS NULL THEN 'Administrativo' ELSE 'Caixa' END AS ORIGEM,
                    TRIM(COALESCE(u.NOME, nc.USUARIO_CANC, '(desconhecido)')) AS RESPONSAVEL
                FROM NOTAS_CAB nc
                LEFT JOIN ABERTURA_CAIXA ac ON ac.COD_ABERT_CAIXA = nc.COD_ABERT_CAIXA
                LEFT JOIN USUARIOS u        ON u.COD_USUARIO     = ac.COD_USUARIO_ABERTURA
                LEFT JOIN PESSOAS p         ON p.COD_PESSOA      = nc.COD_PESSOA
                WHERE nc.DATA_CANC BETWEEN '{$dataIni}' AND '{$dataFim}'
                  AND UPPER(TRIM(COALESCE(u.NOME, nc.USUARIO_CANC, '(desconhecido)'))) = '{$nomeEsc}'
                  {$emp}
                ORDER BY nc.TOTAL_NOTA DESC
            ";
            $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
            $out = [];
            foreach ($rows as $r) {
                $cli = (string)($r['CLIENTE'] ?? '');
                if ($cli && !mb_check_encoding($cli, 'UTF-8')) {
                    $cli = mb_convert_encoding($cli, 'UTF-8', 'Windows-1252');
                }
                $mot = (string)($r['MOTIVO'] ?? '');
                if ($mot && !mb_check_encoding($mot, 'UTF-8')) {
                    $mot = mb_convert_encoding($mot, 'UTF-8', 'Windows-1252');
                }
                $resp = (string)($r['RESPONSAVEL'] ?? '');
                if ($resp && !mb_check_encoding($resp, 'UTF-8')) {
                    $resp = mb_convert_encoding($resp, 'UTF-8', 'Windows-1252');
                }
                $out[] = [
                    'nr_nota'    => (int)$r['NR_NOTA'],
                    'serie'      => trim((string)$r['SERIE']),
                    'data'       => (string)$r['DATA_EMISSAO'],
                    'data_canc'  => (string)$r['DATA_CANC'],
                    'total_nota' => round((float)$r['TOTAL_NOTA'], 2),
                    'cod_pessoa' => (int)($r['COD_PESSOA'] ?? 0),
                    'cliente'    => $cli,
                    'motivo'     => $mot,
                    'origem'     => (string)($r['ORIGEM'] ?? ''),
                    'responsavel'=> $resp,
                ];
            }
            return $out;
        } catch (\Throwable) {
            return [];
        }
    }

    /**
     * Lista as NOTAS com desconto dadas por uma operadora específica.
     * Usado no drill-down (clica na linha → vê as notas).
     */
    public function notasComDescontoDaOperadora(string $dataIni, string $dataFim, string $codOperadora): array
    {
        $emp = $this->filtroEmpresa('nc');
        $codEsc = str_replace("'", "''", trim($codOperadora));
        try {
            $sql = "
                SELECT FIRST 500
                    nc.NR_NOTA,
                    TRIM(nc.SERIE)                                   AS SERIE,
                    nc.DATA_EMISSAO,
                    CAST(nc.TOTAL_NOTA AS DOUBLE PRECISION)          AS TOTAL_NOTA,
                    CAST(nc.DESCONTO   AS DOUBLE PRECISION)          AS DESCONTO,
                    nc.COD_PESSOA,
                    TRIM(p.NOME)                                     AS CLIENTE
                FROM NOTAS_CAB nc
                JOIN ABERTURA_CAIXA ac ON ac.COD_ABERT_CAIXA = nc.COD_ABERT_CAIXA
                LEFT JOIN PESSOAS p    ON p.COD_PESSOA       = nc.COD_PESSOA
                WHERE nc.DATA_EMISSAO BETWEEN '{$dataIni}' AND '{$dataFim}'
                  AND nc.DATA_CANC IS NULL
                  AND nc.DESCONTO > 0
                  AND ac.COD_USUARIO_ABERTURA = '{$codEsc}'
                  {$emp}
                ORDER BY nc.DESCONTO DESC
            ";
            $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
            $out = [];
            foreach ($rows as $r) {
                $cli = (string)($r['CLIENTE'] ?? '');
                if ($cli && !mb_check_encoding($cli, 'UTF-8')) {
                    $cli = mb_convert_encoding($cli, 'UTF-8', 'Windows-1252');
                }
                $tot = (float)$r['TOTAL_NOTA'];
                $des = (float)$r['DESCONTO'];
                $out[] = [
                    'nr_nota'        => (int)$r['NR_NOTA'],
                    'serie'          => trim((string)$r['SERIE']),
                    'data'           => (string)$r['DATA_EMISSAO'],
                    'valor_original' => round($tot + $des, 2),
                    'total_nota'     => round($tot, 2),
                    'desconto'       => round($des, 2),
                    'pct_desconto'   => $tot > 0 ? round($des / ($tot + $des) * 100, 2) : 0,
                    'cod_pessoa'     => (int)($r['COD_PESSOA'] ?? 0),
                    'cliente'        => $cli,
                ];
            }
            return $out;
        } catch (\Throwable) {
            return [];
        }
    }

    // -------------------------------------------------------------------------
    // PERFIL DE LIBERACAO (caixa OU supervisor) — v1.4.34
    // -------------------------------------------------------------------------
    //
    // Painel 360 graus de um operador (papel='solicitante') ou supervisor
    // (papel='supervisor') no periodo, com:
    //   - KPIs comparados com a media da loja inteira no mesmo periodo
    //   - Heatmap dia-da-semana x hora-do-dia
    //   - Evolucao diaria (qtd + valor)
    //   - Mix por tipo de liberacao
    //   - Top 10 clientes (com JOIN PESSOAS)
    //   - Top 3 mensagens
    //   - Top 5 acessos (recurso bloqueado no ERP)
    //   - Distribuicao por STATUS
    //   - Sinais de alerta agregados (concentracao de cliente, horario atipico,
    //     taxa 100% no supervisor, valor crescente, spread baixo)
    //
    // O parametro $papel define a coluna de filtro:
    //   - 'solicitante' -> COD_USUARIO_SOLICITANTE (o caixa que pediu)
    //   - 'supervisor'  -> COD_USUARIO_LIBEROU      (quem autorizou)
    //
    // Read-only Firebird. So SELECT.
    // -------------------------------------------------------------------------
    public function perfilLiberacao(string $codigo, string $papel, string $dataIni, string $dataFim): array
    {
        $papel    = ($papel === 'supervisor') ? 'supervisor' : 'solicitante';
        $col      = ($papel === 'supervisor') ? 'COD_USUARIO_LIBEROU' : 'COD_USUARIO_SOLICITANTE';
        $emp      = $this->filtroEmpresa('l');
        $codEsc   = str_replace("'", "''", trim($codigo));
        // v3: KPIs revistos — Desconto R$ (TIPO=1) x Autorizado R$ (TIPO 2,3),
        // Ticket Desconto honesto (so divide quando tem TIPO=1)
        $cacheKey = "audit_perfil_lib_v3_{$papel}_{$codEsc}_{$dataIni}_{$dataFim}" . $this->escopoCache();

        return Cache::lembrar($cacheKey, $this->ttl($dataFim), function () use ($col, $emp, $codEsc, $dataIni, $dataFim, $papel) {

            $u = fn($s) => ($s !== null && !mb_check_encoding((string)$s, 'UTF-8'))
                ? mb_convert_encoding((string)$s, 'UTF-8', 'Windows-1252') : (string)$s;

            // base WHERE — filtro pessoa
            $whereP = "TRIM(l.{$col}) = '{$codEsc}'
                       AND l.DATA BETWEEN '{$dataIni}' AND '{$dataFim}'
                       {$emp}";

            // base WHERE — filtro loja (mesmo periodo, sem fixar a pessoa)
            $whereL = "l.DATA BETWEEN '{$dataIni}' AND '{$dataFim}'
                       {$emp}";

            // ---- KPIs da pessoa --------------------------------------------------
            // v3: 6 KPIs revistos
            //   1. qtd                — total liberacoes (todos os tipos, mantem)
            //   2. qtd_desconto       — COUNT WHERE TIPO=1
            //   3. val_desconto_real  — SUM(DESCONTO) WHERE TIPO=1 (dinheiro real que saiu)
            //   4. val_autorizado     — SUM(DESCONTO) WHERE TIPO IN (2,3) (fatura/limite, NAO saida)
            //   5. ticket_desconto    — val_desconto_real / qtd_desconto (null se qtd_desconto=0)
            //   6. taxa_liberacao     — mantem (% status L)
            $kpisPessoa = [
                'qtd'               => 0,
                'qtd_desconto'      => 0,
                'val_desconto_real' => 0.0,
                'val_autorizado'    => 0.0,
                'ticket_desconto'   => null,
                // legados (mantidos pra compat com partes antigas da view)
                'valor_total'       => 0.0,
                'ticket_medio'      => 0.0,
                'qtd_liberadas'     => 0,
                'taxa_liberacao'    => 0.0,
            ];
            try {
                $sql = "
                    SELECT
                        COUNT(*) AS QTD,
                        COUNT(CASE WHEN l.TIPO_LIBERACAO=1 THEN 1 END) AS QTDD,
                        COALESCE(SUM(CASE WHEN l.TIPO_LIBERACAO=1
                                          THEN CAST(l.DESCONTO AS DOUBLE PRECISION) ELSE 0 END), 0) AS VDESC,
                        COALESCE(SUM(CASE WHEN l.TIPO_LIBERACAO IN (2,3)
                                          THEN CAST(l.DESCONTO AS DOUBLE PRECISION) ELSE 0 END), 0) AS VAUT,
                        COALESCE(SUM(l.DESCPERM), 0) AS VTOT_PERMIT,
                        SUM(CASE WHEN TRIM(l.STATUS) = 'L' THEN 1 ELSE 0 END) AS QLIB
                    FROM LIBERACOES l
                    WHERE {$whereP}
                ";
                $r = $this->pdo->query($sql)->fetch(\PDO::FETCH_ASSOC) ?: [];
                $qtd      = (int)($r['QTD'] ?? 0);
                $qtdDesc  = (int)($r['QTDD'] ?? 0);
                $vDesc    = round((float)($r['VDESC'] ?? 0), 2);
                $vAut     = round((float)($r['VAUT']  ?? 0), 2);
                $vPermit  = round((float)($r['VTOT_PERMIT'] ?? 0), 2);
                $qLib     = (int)($r['QLIB'] ?? 0);
                $ticDesc  = $qtdDesc > 0 ? round($vDesc / $qtdDesc, 2) : null;
                $kpisPessoa = [
                    'qtd'               => $qtd,
                    'qtd_desconto'      => $qtdDesc,
                    'val_desconto_real' => $vDesc,
                    'val_autorizado'    => $vAut,
                    'ticket_desconto'   => $ticDesc,
                    // legados — antes apontavam pra SUM(DESCPERM) total; agora valor_total
                    // aponta pra Desconto R$ (dinheiro real). Telas antigas continuam funcionando
                    // mas mostrando o numero honesto, nao mais o inflado.
                    'valor_total'       => $vDesc,
                    'ticket_medio'      => $ticDesc ?? 0.0,
                    'qtd_liberadas'     => $qLib,
                    'taxa_liberacao'    => $qtd > 0 ? round($qLib / $qtd * 100, 1) : 0.0,
                ];
            } catch (\Throwable) {}

            // ---- KPIs da loja (media por pessoa no mesmo periodo) ---------------
            // v3: ticket_medio_loja agora reflete TICKET DESCONTO HONESTO
            // (SUM DESCONTO WHERE TIPO=1) / (COUNT WHERE TIPO=1), agregado pela loja.
            $kpisLoja = [
                'qtd_media'         => 0.0,
                'valor_medio'       => 0.0,
                'ticket_medio_loja' => 0.0,
                'taxa_media'        => 0.0,
                'qtd_pessoas'       => 0,
            ];
            try {
                $sql = "
                    SELECT
                        AVG(QTD_P)    AS QM,
                        AVG(VAL_P)    AS VM,
                        AVG(TX)       AS TX,
                        COUNT(*)      AS QPES,
                        SUM(VDESC_P)  AS SUM_VDESC,
                        SUM(QDESC_P)  AS SUM_QDESC
                    FROM (
                        SELECT
                            TRIM(l.{$col}) AS QUEM,
                            COUNT(*) AS QTD_P,
                            SUM(CASE WHEN l.TIPO_LIBERACAO=1
                                     THEN CAST(l.DESCONTO AS DOUBLE PRECISION) ELSE 0 END) AS VDESC_P,
                            COUNT(CASE WHEN l.TIPO_LIBERACAO=1 THEN 1 END) AS QDESC_P,
                            SUM(l.DESCPERM) AS VAL_P,
                            CAST(SUM(CASE WHEN TRIM(l.STATUS)='L' THEN 1 ELSE 0 END) AS DOUBLE PRECISION)
                                / NULLIF(COUNT(*), 0) * 100 AS TX
                        FROM LIBERACOES l
                        WHERE {$whereL}
                        GROUP BY TRIM(l.{$col})
                    ) agg
                ";
                $r = $this->pdo->query($sql)->fetch(\PDO::FETCH_ASSOC) ?: [];
                $sumVDesc = (float)($r['SUM_VDESC'] ?? 0);
                $sumQDesc = (int)  ($r['SUM_QDESC'] ?? 0);
                $ticketLoja = $sumQDesc > 0 ? ($sumVDesc / $sumQDesc) : 0.0;
                $kpisLoja = [
                    'qtd_media'         => round((float)($r['QM'] ?? 0), 1),
                    'valor_medio'       => round((float)($r['VM'] ?? 0), 2),
                    'ticket_medio_loja' => round($ticketLoja, 2),
                    'taxa_media'        => round((float)($r['TX'] ?? 0), 1),
                    'qtd_pessoas'       => (int)($r['QPES']      ?? 0),
                ];
            } catch (\Throwable) {}

            // ---- Heatmap dia-da-semana x hora -----------------------------------
            // Matriz 7 (dom-sab) x 24 (hora), inicia zerada
            $heatmap = [];
            for ($d = 0; $d < 7; $d++) {
                for ($h = 0; $h < 24; $h++) {
                    $heatmap[] = ['dia' => $d, 'hora' => $h, 'qtd' => 0, 'valor' => 0.0];
                }
            }
            try {
                // v3: VTOT agora soma DESCONTO so quando TIPO=1 (Desconto R$ real)
                $sql = "
                    SELECT
                        EXTRACT(WEEKDAY FROM l.DATA) AS DS,
                        CAST(SUBSTRING(CAST(l.HORA AS VARCHAR(8)) FROM 1 FOR 2) AS INTEGER) AS HD,
                        COUNT(*) AS QTD,
                        COALESCE(SUM(CASE WHEN l.TIPO_LIBERACAO=1
                                          THEN CAST(l.DESCONTO AS DOUBLE PRECISION) ELSE 0 END), 0) AS VTOT
                    FROM LIBERACOES l
                    WHERE {$whereP}
                    GROUP BY 1, 2
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
                $idx = [];
                foreach ($heatmap as $k => $cell) {
                    $idx[$cell['dia'] . '_' . $cell['hora']] = $k;
                }
                foreach ($rows as $r) {
                    $d = (int)($r['DS'] ?? -1);
                    $h = (int)($r['HD'] ?? -1);
                    if ($d < 0 || $d > 6 || $h < 0 || $h > 23) continue;
                    $k = $idx[$d . '_' . $h] ?? null;
                    if ($k === null) continue;
                    $heatmap[$k]['qtd']   = (int)$r['QTD'];
                    $heatmap[$k]['valor'] = round((float)$r['VTOT'], 2);
                }
            } catch (\Throwable) {}

            // ---- Evolucao diaria -------------------------------------------------
            // v3: VTOT agora soma Desconto R$ real (TIPO=1), nao mais DESCPERM total
            $evolucao = [];
            try {
                $sql = "
                    SELECT
                        l.DATA AS DIA,
                        COUNT(*) AS QTD,
                        COALESCE(SUM(CASE WHEN l.TIPO_LIBERACAO=1
                                          THEN CAST(l.DESCONTO AS DOUBLE PRECISION) ELSE 0 END), 0) AS VTOT
                    FROM LIBERACOES l
                    WHERE {$whereP}
                    GROUP BY l.DATA
                    ORDER BY l.DATA
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as $r) {
                    $evolucao[] = [
                        'data'  => (string)$r['DIA'],
                        'qtd'   => (int)$r['QTD'],
                        'valor' => round((float)$r['VTOT'], 2),
                    ];
                }
            } catch (\Throwable) {}

            // ---- Mix por tipo ----------------------------------------------------
            // v3: VTOT por tipo agora usa SUM(DESCONTO) — para TIPO=1 e o desconto real,
            // para TIPO 2/3 e o valor autorizado, para TIPO=0 e zero (sem componente monetario).
            $mixTipo = [];
            try {
                $sql = "
                    SELECT l.TIPO_LIBERACAO AS T,
                           COUNT(*) AS QTD,
                           COALESCE(SUM(CAST(l.DESCONTO AS DOUBLE PRECISION)), 0) AS VTOT
                    FROM LIBERACOES l
                    WHERE {$whereP}
                    GROUP BY l.TIPO_LIBERACAO
                    ORDER BY 2 DESC
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as $r) {
                    $mixTipo[] = [
                        'tipo'  => (int)($r['T'] ?? 0),
                        'qtd'   => (int)$r['QTD'],
                        'valor' => round((float)$r['VTOT'], 2),
                    ];
                }
            } catch (\Throwable) {}

            // ---- Top 10 clientes -------------------------------------------------
            // v3: VTOT por cliente agora soma Desconto R$ real (TIPO=1)
            $topClientes = [];
            try {
                $sql = "
                    SELECT FIRST 10
                        l.COD_PESSOA AS CP,
                        TRIM(COALESCE(p.NOME, 'Sem cliente')) AS NOME,
                        COUNT(*) AS QTD,
                        COALESCE(SUM(CASE WHEN l.TIPO_LIBERACAO=1
                                          THEN CAST(l.DESCONTO AS DOUBLE PRECISION) ELSE 0 END), 0) AS VTOT
                    FROM LIBERACOES l
                    LEFT JOIN PESSOAS p ON p.COD_PESSOA = l.COD_PESSOA
                    WHERE {$whereP}
                    GROUP BY l.COD_PESSOA, p.NOME
                    ORDER BY 3 DESC
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as $r) {
                    $topClientes[] = [
                        'cod_pessoa' => (int)($r['CP'] ?? 0),
                        'nome'       => $u(trim((string)($r['NOME'] ?? ''))),
                        'qtd'        => (int)$r['QTD'],
                        'valor'      => round((float)$r['VTOT'], 2),
                    ];
                }
            } catch (\Throwable) {}

            // ---- Top 3 mensagens -------------------------------------------------
            $topMensagens = [];
            try {
                $sql = "
                    SELECT FIRST 3 TRIM(l.MENSAGEM) AS MSG, COUNT(*) AS QTD
                    FROM LIBERACOES l
                    WHERE {$whereP}
                      AND l.MENSAGEM IS NOT NULL
                      AND TRIM(l.MENSAGEM) <> ''
                    GROUP BY TRIM(l.MENSAGEM)
                    ORDER BY 2 DESC
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as $r) {
                    $topMensagens[] = [
                        'mensagem' => $u(trim((string)($r['MSG'] ?? ''))),
                        'qtd'      => (int)$r['QTD'],
                    ];
                }
            } catch (\Throwable) {}

            // ---- Top 5 acessos (recurso bloqueado) -------------------------------
            $topAcessos = [];
            try {
                $sql = "
                    SELECT FIRST 5 TRIM(l.ACESSO) AS AC, COUNT(*) AS QTD
                    FROM LIBERACOES l
                    WHERE {$whereP}
                      AND l.ACESSO IS NOT NULL
                      AND TRIM(l.ACESSO) <> ''
                    GROUP BY TRIM(l.ACESSO)
                    ORDER BY 2 DESC
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as $r) {
                    $topAcessos[] = [
                        'acesso' => $u(trim((string)($r['AC'] ?? ''))),
                        'qtd'    => (int)$r['QTD'],
                    ];
                }
            } catch (\Throwable) {}

            // ---- Distribuicao por status -----------------------------------------
            $statusDist = [];
            try {
                $sql = "
                    SELECT TRIM(l.STATUS) AS ST, COUNT(*) AS QTD
                    FROM LIBERACOES l
                    WHERE {$whereP}
                    GROUP BY TRIM(l.STATUS)
                ";
                $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
                foreach ($rows as $r) {
                    $statusDist[] = [
                        'status' => trim((string)($r['ST'] ?? '')),
                        'qtd'    => (int)$r['QTD'],
                    ];
                }
            } catch (\Throwable) {}

            // ---- Sinais de alerta ------------------------------------------------
            $alerta = $this->calcularSinaisAlerta([
                'kpis_pessoa'  => $kpisPessoa,
                'kpis_loja'    => $kpisLoja,
                'top_clientes' => $topClientes,
                'heatmap'      => $heatmap,
                'evolucao'     => $evolucao,
                'papel'        => $papel,
            ]);

            return [
                'codigo'        => $codEsc,
                'papel'         => $papel,
                'data_ini'      => $dataIni,
                'data_fim'      => $dataFim,
                'kpis_pessoa'   => $kpisPessoa,
                'kpis_loja'     => $kpisLoja,
                'heatmap'       => $heatmap,
                'evolucao'      => $evolucao,
                'mix_tipo'      => $mixTipo,
                'top_clientes'  => $topClientes,
                'top_mensagens' => $topMensagens,
                'top_acessos'   => $topAcessos,
                'status_dist'   => $statusDist,
                'alerta'        => $alerta,
            ];
        });
    }

    /**
     * Calcula sinais de alerta agregados a partir do perfil ja montado.
     * Retorna ['nivel' => 'ok|atencao|alerta', 'motivos' => [string,...]]
     */
    private function calcularSinaisAlerta(array $p): array
    {
        $motivos  = [];
        $pessoa   = $p['kpis_pessoa']  ?? [];
        $loja     = $p['kpis_loja']    ?? [];
        $topCli   = $p['top_clientes'] ?? [];
        $heatmap  = $p['heatmap']      ?? [];
        $evol     = $p['evolucao']     ?? [];
        $papel    = $p['papel']        ?? 'solicitante';

        $qtdTotal = (int)($pessoa['qtd'] ?? 0);

        // 1) CONCENTRACAO DE CLIENTE: cliente #1 >= 40% e ao menos 5 ocorrencias
        if (!empty($topCli) && $qtdTotal >= 5) {
            $primeiro = $topCli[0]['qtd'] ?? 0;
            if ($primeiro >= 5 && $qtdTotal > 0) {
                $pct = $primeiro / $qtdTotal * 100;
                if ($pct >= 40) {
                    $motivos[] = 'Concentracao alta: cliente #' . ($topCli[0]['nome'] ?: '?')
                               . ' tem ' . number_format($pct, 1, ',', '.') . '% das liberacoes';
                }
            }
        }

        // 2) HORARIO ATIPICO: >= 30% fora de 08h-22h ou em domingo
        if ($qtdTotal >= 5 && !empty($heatmap)) {
            $fora = 0;
            foreach ($heatmap as $c) {
                $h = $c['hora'];
                $d = $c['dia'];
                if ($h < 8 || $h > 22 || $d === 0) {
                    $fora += (int)$c['qtd'];
                }
            }
            $pctFora = $qtdTotal > 0 ? $fora / $qtdTotal * 100 : 0;
            if ($pctFora >= 30) {
                $motivos[] = 'Horario atipico: ' . number_format($pctFora, 1, ',', '.')
                           . '% das liberacoes fora do horario comercial (08h-22h) ou em domingo';
            }
        }

        // 3) TAXA 100% (supervisor com volume >= 10)
        if ($papel === 'supervisor' && $qtdTotal >= 10) {
            $taxa = (float)($pessoa['taxa_liberacao'] ?? 0);
            if ($taxa >= 99.99) {
                $motivos[] = 'Supervisor libera 100% do que recebe (volume ' . $qtdTotal . ')';
            }
        }

        // 4) VALOR CRESCENTE: media dos ultimos 7 dias >= 1,5x media dos primeiros 7
        if (count($evol) >= 8) {
            $n = count($evol);
            $primeiros = array_slice($evol, 0, 7);
            $ultimos   = array_slice($evol, max(0, $n - 7));
            $mp = 0.0; $mu = 0.0;
            foreach ($primeiros as $e) { $mp += (float)$e['valor']; }
            foreach ($ultimos as $e)   { $mu += (float)$e['valor']; }
            $mp /= count($primeiros);
            $mu /= count($ultimos);
            if ($mp > 0 && $mu >= 1.5 * $mp) {
                $motivos[] = 'Valor crescente: ultimos 7 dias com media '
                           . number_format($mu / $mp, 1, ',', '.') . 'x maior que o inicio do periodo';
            }
        }

        // 5) VOLUME ANOMALO vs media da loja
        $qm = (float)($loja['qtd_media'] ?? 0);
        if ($qm > 0 && $qtdTotal >= 2 * $qm) {
            $vezes = $qm > 0 ? $qtdTotal / $qm : 0;
            $motivos[] = 'Volume ' . number_format($vezes, 1, ',', '.')
                       . 'x acima da media da loja (' . number_format($qm, 1, ',', '.') . ' por pessoa)';
        }

        // 6) TICKET MUITO ALTO vs loja
        $tp = (float)($pessoa['ticket_medio']    ?? 0);
        $tl = (float)($loja['ticket_medio_loja'] ?? 0);
        if ($tl > 0 && $tp >= 2 * $tl && $qtdTotal >= 5) {
            $motivos[] = 'Ticket medio ' . number_format($tp / $tl, 1, ',', '.')
                       . 'x maior que a media da loja';
        }

        $n = count($motivos);
        $nivel = 'ok';
        if ($n >= 3)      $nivel = 'alerta';
        elseif ($n >= 2)  $nivel = 'atencao';
        elseif ($n === 1) $nivel = 'atencao';

        return ['nivel' => $nivel, 'motivos' => $motivos];
    }
}
