<?php

namespace App\Services;

use App\Repositories\FechamentoCaixaRepository;

/**
 * Regras de negócio do módulo Fechamento de Caixa.
 * Monta a estrutura completa de dados para o card do dashboard,
 * calculando status, divergências, comparativos e indicadores.
 */
class FechamentoCaixaService
{
    private FechamentoCaixaRepository $repo;

    // Limiar de divergência: abaixo = verde, acima = amarelo, acima do crítico = vermelho
    private const DIVERGENCIA_AMARELO  = 0.50;
    private const DIVERGENCIA_VERMELHO = 50.00;

    public function __construct()
    {
        $this->repo = new FechamentoCaixaRepository();
    }

    // =========================================================================
    // MÉTODO PRINCIPAL — monta estrutura completa para o JSON
    // =========================================================================

    /**
     * Retorna a lista completa de fechamentos com todos os dados do card.
     *
     * OTIMIZAÇÃO: usa batch queries — ~10 queries fixas independente do número
     * de fechamentos (antes era 10×N queries para N fechamentos).
     */
    public function fechamentosCompletos(string $busca, string $dataIni, string $dataFim): array
    {
        $fechamentos = $this->repo->listarFechamentos($busca, $dataIni, $dataFim);
        if (empty($fechamentos)) return [];

        // ── 1. Prepara metadados de todos os fechamentos ─────────────────────
        $caixaIds   = [];
        $metaDados  = []; // [id => [...]]

        // Calcula range de datas global para usar nas batch queries
        $minData = null;
        $maxData = null;

        foreach ($fechamentos as $fc) {
            $id  = (int)$fc['COD_ABERT_CAIXA'];
            $dAb = $this->extrairData($fc['DATA_ABERTURA']);
            $dFe = $fc['DATA_FECHAMENTO'] ? $this->extrairData($fc['DATA_FECHAMENTO']) : $dAb;

            $caixaIds[] = $id;
            $metaDados[$id] = [
                'fc'  => $fc,
                'dAb' => $dAb,
                'dFe' => $dFe,
            ];

            if ($minData === null || $dAb < $minData) $minData = $dAb;
            if ($maxData === null || $dFe > $maxData) $maxData = $dFe;
        }

        $minData ??= $dataIni;
        $maxData ??= $dataFim;

        // ── 2. Batch queries — 1 round-trip cada ─────────────────────────────
        // 2a. Notas por caixa [caixaId => [nrNotas]]
        $notasPorCaixa = $this->repo->nrNotasBatch($caixaIds);

        // Monta mapa invertido nota→caixa (necessário para batch por nota)
        $notaCaixaMap = [];
        foreach ($notasPorCaixa as $cid => $notas) {
            foreach ($notas as $nr) {
                $notaCaixaMap[(int)$nr] = $cid;
            }
        }

        // 2b. Formas sistema [caixaId => [rows]]
        $formasSistemaBatch = $this->repo->formasPorVendasBatch($notaCaixaMap, $minData, $maxData);

        // 2c. Formas operador [caixaId => [rows]]
        $formasOperadorBatch = $this->repo->formasOperadorBatch($caixaIds);

        // 2d. Sangrias [caixaId => [rows]]
        $sangriasBatch = $this->repo->sangriasBatch($caixaIds);

        // 2e. Suprimentos [caixaId => [rows]]
        $suprimentosBatch = $this->repo->suprimentosBatch($caixaIds);

        // 2f. Cancelamentos [caixaId => [rows]]
        $cancelamentosBatch = $this->repo->cancelamentosBatch($caixaIds);

        // 2g. Devoluções [caixaId => [rows]]
        $devolucoesBatch = $this->repo->devolucoesBatch($caixaIds);

        // 2h. Resumo vendas [caixaId => resumo]
        $resumoBatch = $this->repo->resumoVendasBatch($notaCaixaMap);

        // 2i. Dados fiscais [caixaId => [vendas]]
        $fiscalBatch = $this->repo->vendasFiscalBatch($notaCaixaMap, $minData, $maxData);

        // ── 3. Monta resultado por fechamento (apenas PHP, sem queries) ───────
        $resultado = [];
        $vazioResumo = ['QTD_NOTAS' => 0, 'TOTAL' => 0, 'TICKET_MEDIO' => 0, 'MAIOR_VENDA' => 0];

        foreach ($fechamentos as $fc) {
            $id       = (int)$fc['COD_ABERT_CAIXA'];
            $operador = $fc['OPERADOR'];

            // Valores financeiros
            $suprimento   = (float)$fc['SUPRIMENTO'];
            $valCalculado = (float)$fc['VALOR_CALCULADO'];
            $valInformado = (float)$fc['VALOR_INFORMADO'];
            $diferenca    = $valInformado - $valCalculado;
            $valLiquido   = $valInformado - $suprimento;

            $status = $this->calcularStatus($fc['STATUS'], $diferenca);
            $cor    = $this->calcularCor($diferenca);

            $formasSistema  = $formasSistemaBatch[$id]  ?? [];
            $formasOperador = $formasOperadorBatch[$id] ?? [];
            $sangrias       = $sangriasBatch[$id]       ?? [];
            $suprimentos    = $suprimentosBatch[$id]    ?? [];
            $resumo         = $resumoBatch[$id]         ?? $vazioResumo;

            // Cancelamentos: filtra por operador em PHP (evita query extra)
            $opUpper = strtoupper(trim($operador));
            $cancelamentos = array_filter(
                $cancelamentosBatch[$id] ?? [],
                fn($r) => strtoupper(trim($r['USUARIO'])) === $opUpper
            );
            $devolucoes = array_filter(
                $devolucoesBatch[$id] ?? [],
                fn($r) => strtoupper(trim($r['USUARIO'])) === $opUpper
            );

            $rawFiscal = $fiscalBatch[$id] ?? [];

            // Estruturas calculadas
            $comparativo = $this->montarComparativo($formasSistema, $formasOperador);
            $indicadores = $this->montarIndicadores($formasSistema, $resumo, array_values($cancelamentos), array_values($devolucoes), $diferenca);

            $resultado[] = [
                // --- Identificação ---
                'id'                => $id,
                'operador'          => $operador,
                'operador_abertura' => $fc['OPERADOR_ABERTURA'] ?? $operador,
                'terminal'          => $fc['TERMINAL'] ?: 'Não informado',

                // --- Datas formatadas ---
                'data_abertura'    => $this->formatarDH($fc['DATA_ABERTURA'],   $fc['HORA_ABERTURA']),
                'data_fechamento'  => $this->formatarDH($fc['DATA_FECHAMENTO'], $fc['HORA_FECHAMENTO']),

                // --- Status ---
                'status'           => $status,
                'status_cor'       => $cor,

                // --- Valores financeiros ---
                'suprimento'       => round($suprimento,   2),
                'total_calculado'  => round($valCalculado, 2),
                'total_informado'  => round($valInformado, 2),
                'valor_liquido'    => round($valLiquido,   2),
                'diferenca'        => round($diferenca,    2),

                // --- Seções do card ---
                'resumo_pagamentos'=> $this->resumoFormas($formasSistema),
                'formas_sistema'   => $this->formatarFormasSistema($formasSistema),
                'formas_operador'  => $this->formatarFormasOperador($formasOperador),
                'comparativo'      => $comparativo,
                'sangrias'         => $this->formatarMovimentos($sangrias),
                'suprimentos'      => $this->formatarMovimentos($suprimentos),
                'cancelamentos'    => $this->formatarCancelamentos(array_values($cancelamentos)),
                'devolucoes'       => $this->formatarDevolucoes(array_values($devolucoes)),
                'vendas_fiscal'    => $this->analiseFiscal($rawFiscal),
                'indicadores'      => $indicadores,
            ];
        }

        return $resultado;
    }

    // =========================================================================
    // STATUS E COR DO INDICADOR
    // =========================================================================

    /**
     * Calcula o status textual do fechamento:
     * - 'aberto'     → STATUS='A' no banco
     * - 'fechado'    → STATUS='F' e diferença dentro do tolerável
     * - 'divergente' → STATUS='F' e diferença acima do tolerável
     */
    private function calcularStatus(string $statusBanco, float $diferenca): string
    {
        if ($statusBanco !== 'F') return 'aberto';
        return abs($diferenca) < self::DIVERGENCIA_AMARELO ? 'fechado' : 'divergente';
    }

    /**
     * Retorna a cor do indicador visual:
     * - 'verde'     → sem diferença
     * - 'amarelo'   → diferença pequena
     * - 'vermelho'  → diferença crítica
     */
    private function calcularCor(float $diferenca): string
    {
        $abs = abs($diferenca);
        if ($abs < self::DIVERGENCIA_AMARELO)  return 'verde';
        if ($abs < self::DIVERGENCIA_VERMELHO) return 'amarelo';
        return 'vermelho';
    }

    // =========================================================================
    // RESUMO DE PAGAMENTOS — agrupado por categoria para o fiscal
    // =========================================================================

    /**
     * Classifica uma forma de pagamento em grupo visual para o fiscal.
     * Baseado no DESCRICAO + TIPO da tabela FORMA_PGTO.
     */
    private function classificarGrupo(string $descricao, string $tipo): string
    {
        $d = strtoupper(trim($descricao));

        if ($tipo === '1') return 'Dinheiro';
        if ($tipo === '8') return 'PIX';

        if ($tipo === '3') {
            $temTef  = str_contains($d, 'TEF');
            $temPos  = str_contains($d, 'POS');
            $temCred = (bool) preg_match('/CRED|CRÉDITO/u', $d);
            $temDeb  = (bool) preg_match('/DEB|DÉBITO/u', $d);

            if ($temTef && $temCred) return 'TEF Crédito';
            if ($temTef && $temDeb)  return 'TEF Débito';
            if ($temPos && $temCred) return 'POS Crédito';
            if ($temPos && $temDeb)  return 'POS Débito';
            return 'Convênio/Outros';   // ASPM, GRANCARD, SINTIA, SINTRO, etc.
        }

        return 'Outros';  // PRAZO, BOLETO, CREDIÁRIO, CHEQUE, VALE
    }

    /**
     * Monta resumo de pagamentos agrupado por categoria (para o fiscal de caixa).
     * Ordem fixa: Dinheiro → PIX → TEF Débito → TEF Crédito → POS Débito → POS Crédito → resto
     */
    private function resumoFormas(array $formasSistema): array
    {
        $ordemGrupos = [
            'Dinheiro'       => ['icon' => '💵', 'color' => '#4ade80'],
            'PIX'            => ['icon' => '📱', 'color' => '#a78bfa'],
            'TEF Débito'     => ['icon' => '🏧', 'color' => '#34d399'],
            'TEF Crédito'    => ['icon' => '💳', 'color' => '#60a5fa'],
            'POS Débito'     => ['icon' => '📟', 'color' => '#86efac'],
            'POS Crédito'    => ['icon' => '📟', 'color' => '#93c5fd'],
            'Convênio/Outros'=> ['icon' => '🏷️', 'color' => '#fbbf24'],
            'Outros'         => ['icon' => '📄', 'color' => '#94a3b8'],
        ];

        $totais = [];
        foreach ($formasSistema as $f) {
            $grupo  = $this->classificarGrupo((string)($f['DESCRICAO'] ?? ''), (string)($f['TIPO'] ?? ''));
            $valor  = (float)$f['ENTRADAS'] - (float)$f['SAIDAS'];
            $totais[$grupo] = ($totais[$grupo] ?? 0.0) + $valor;
        }

        $resultado = [];
        foreach ($ordemGrupos as $grupo => $meta) {
            if (!isset($totais[$grupo])) continue;
            $resultado[] = [
                'grupo' => $grupo,
                'icon'  => $meta['icon'],
                'color' => $meta['color'],
                'total' => round($totais[$grupo], 2),
            ];
        }

        // Total geral para calcular %
        $totalGeral = array_sum(array_column($resultado, 'total'));
        foreach ($resultado as &$r) {
            $r['pct'] = $totalGeral > 0 ? round($r['total'] / $totalGeral * 100, 1) : 0.0;
        }
        unset($r);

        return $resultado;
    }

    // =========================================================================
    // SEÇÃO 2 — Formas de pagamento do sistema
    // =========================================================================

    private function formatarFormasSistema(array $rows): array
    {
        return array_map(function ($r) {
            $entradas = (float)$r['ENTRADAS'];
            $saidas   = (float)$r['SAIDAS'];
            $liquido  = $entradas - $saidas;
            $desc     = strtoupper(trim((string)($r['DESCRICAO'] ?? '')));

            // Classifica subtipo de cartão para o frontend exibir ícone correto
            $subtipo = '';
            if ((string)$r['TIPO'] === '3') {
                if (preg_match('/CR[EÉ]D|CREDIT/u', $desc))              $subtipo = 'credito';
                elseif (preg_match('/D[EÉ]B|DEBIT/u', $desc))            $subtipo = 'debito';
                elseif (preg_match('/VOUCHER|BENEF|VALE/u', $desc))       $subtipo = 'voucher';
                else                                                       $subtipo = 'tef';
            }

            return [
                'cod'           => $r['COD'],
                'descricao'     => $r['DESCRICAO'],
                'tipo'          => $r['TIPO'],
                'subtipo'       => $subtipo,
                'entradas'      => round($entradas, 2),
                'saidas'        => round($saidas,   2),
                'total_liquido' => round($liquido,  2),
            ];
        }, $rows);
    }

    // =========================================================================
    // SEÇÃO 3 — Formas de pagamento do operador
    // =========================================================================

    private function formatarFormasOperador(array $rows): array
    {
        return array_map(fn($r) => [
            'cod'             => $r['COD'],
            'descricao'       => $r['DESCRICAO'],
            'tipo'            => $r['TIPO'],
            'valor_informado' => round((float)$r['VALOR_INFORMADO'], 2),
        ], $rows);
    }

    // =========================================================================
    // SEÇÃO 4 — Comparativo sistema × operador
    // =========================================================================

    /**
     * Mescla as duas listas de formas e calcula diferença e percentual por linha.
     */
    private function montarComparativo(array $sistema, array $operador): array
    {
        // Indexa sistema por descrição
        $mapSistema = [];
        foreach ($sistema as $s) {
            $key = $s['DESCRICAO'];
            $mapSistema[$key] = (float)$s['ENTRADAS'] - (float)$s['SAIDAS'];
        }

        // Indexa operador por descrição
        $mapOp = [];
        foreach ($operador as $o) {
            $mapOp[$o['DESCRICAO']] = (float)$o['VALOR_INFORMADO'];
        }

        // Une todas as chaves
        $todasChaves = array_unique(array_merge(array_keys($mapSistema), array_keys($mapOp)));
        sort($todasChaves);

        $resultado = [];
        foreach ($todasChaves as $descricao) {
            $apurado  = $mapSistema[$descricao] ?? 0;
            $informado = $mapOp[$descricao]    ?? 0;
            $diff     = $informado - $apurado;
            $perc     = $apurado != 0 ? round(($diff / $apurado) * 100, 2) : 0;
            $cor      = $this->calcularCor($diff);

            $resultado[] = [
                'condicao'       => $descricao,
                'valor_apurado'  => round($apurado,  2),
                'valor_informado'=> round($informado, 2),
                'diferenca'      => round($diff,      2),
                'percentual'     => $perc,
                'cor'            => $cor,
            ];
        }

        return $resultado;
    }

    // =========================================================================
    // SEÇÃO 5 — Sangrias e suprimentos
    // =========================================================================

    private function formatarMovimentos(array $rows): array
    {
        return array_map(function ($r) {
            $dh = $r['DATA_HORA_LANC'];
            if ($dh instanceof \DateTime) $dh = $dh->format('d/m/Y H:i');
            else {
                $s = trim((string)$dh);
                if (strlen($s) >= 16) {
                    $dh = substr($s, 8, 2) . '/' . substr($s, 5, 2) . '/' . substr($s, 0, 4)
                        . ' ' . substr($s, 11, 5);
                }
            }
            return [
                'seq'       => $r['SEQ_LANC'],
                'data_hora' => $dh,
                'valor'     => round((float)$r['VALOR'], 2),
                'historico' => $r['HISTORICO'],
                'usuario'   => $r['USUARIO'],
            ];
        }, $rows);
    }

    // =========================================================================
    // SEÇÃO 6 — Cancelamentos
    // =========================================================================

    private function formatarCancelamentos(array $rows): array
    {
        return array_map(function ($r) {
            return [
                'nr_nota'       => $r['NR_NOTA'],
                'data_emissao'  => $this->extrairData($r['DATA_EMISSAO']),
                'data_canc'     => $this->extrairData($r['DATA_CANC']),
                'total'         => round((float)$r['TOTAL_NOTA'], 2),
                'operador'      => $r['USUARIO'],
                'obs'           => $r['OBS'],
            ];
        }, $rows);
    }

    // =========================================================================
    // SEÇÃO 7 — Devoluções
    // =========================================================================

    private function formatarDevolucoes(array $rows): array
    {
        return array_map(function ($r) {
            return [
                'nr_nota'        => $r['NR_NOTA'],
                'data_emissao'   => $this->extrairData($r['DATA_EMISSAO']),
                'total'          => round((float)$r['TOTAL_NOTA'], 2),
                'operador'       => $r['USUARIO'],
                'obs'            => $r['OBS'],
                'nr_nota_origem' => $r['NR_NOTA_ORIGEM'] ?? '',
            ];
        }, $rows);
    }

    // =========================================================================
    // SEÇÃO 8 — Indicadores
    // =========================================================================

    /**
     * Monta os mini-cards de indicadores do turno.
     */
    private function montarIndicadores(
        array $formasSistema,
        array $resumo,
        array $cancelamentos,
        array $devolucoes,
        float $diferenca
    ): array {
        $totalDinheiro = 0;
        $totalCartoes  = 0;
        $totalCredito  = 0;
        $totalDebito   = 0;
        $totalVoucher  = 0;
        $totalPix      = 0;
        $formasCartao  = [];  // detalhamento individual de cada forma de cartão

        foreach ($formasSistema as $f) {
            $liquido = (float)$f['ENTRADAS'] - (float)$f['SAIDAS'];
            $desc    = strtoupper(trim((string)($f['DESCRICAO'] ?? '')));
            switch ((string)$f['TIPO']) {
                case '1':
                    $totalDinheiro += $liquido;
                    break;
                case '3':
                    $totalCartoes += $liquido;
                    // Classifica crédito / débito / voucher pelo nome da forma
                    if (preg_match('/CR[EÉ]D|CREDIT/u', $desc)) {
                        $totalCredito += $liquido;
                        $subTipo = 'credito';
                    } elseif (preg_match('/D[EÉ]B|DEBIT/u', $desc)) {
                        $totalDebito += $liquido;
                        $subTipo = 'debito';
                    } elseif (preg_match('/VOUCHER|BENEF|VALE/u', $desc)) {
                        $totalVoucher += $liquido;
                        $subTipo = 'voucher';
                    } else {
                        $subTipo = 'tef';
                    }
                    $formasCartao[] = [
                        'descricao' => $f['DESCRICAO'],
                        'subtipo'   => $subTipo,
                        'total'     => round($liquido, 2),
                    ];
                    break;
                case '8':
                    $totalPix += $liquido;
                    break;
            }
        }

        $totalCancelado  = array_sum(array_column($cancelamentos, 'total'));
        $totalDevolucoes = array_sum(array_column($devolucoes,    'total'));

        return [
            'total_dinheiro'   => round($totalDinheiro,   2),
            'total_cartoes'    => round($totalCartoes,    2),
            'total_credito'    => round($totalCredito,    2),
            'total_debito'     => round($totalDebito,     2),
            'total_voucher'    => round($totalVoucher,    2),
            'total_pix'        => round($totalPix,        2),
            'formas_cartao'    => $formasCartao,
            'total_cancelado'  => round($totalCancelado,  2),
            'total_devolucoes' => round($totalDevolucoes, 2),
            'qtd_vendas'       => (int)($resumo['QTD_NOTAS']    ?? 0),
            'ticket_medio'     => round((float)($resumo['TICKET_MEDIO'] ?? 0), 2),
            'maior_venda'      => round((float)($resumo['MAIOR_VENDA']  ?? 0), 2),
            'divergencia'      => round(abs($diferenca), 2),
        ];
    }

    // =========================================================================
    // SEÇÃO 9 — Análise Fiscal (com nota / sem nota)
    // =========================================================================

    /**
     * Classifica cada venda como COM_NOTA ou SEM_NOTA e calcula
     * totais, percentuais e indicador de Risco Fiscal.
     *
     * Critério: tem documento fiscal se NR_SAT > 0 OU CHAVE_SAT não vazia
     *           OU NR_CUPOM > 0.
     * Se nenhum campo fiscal existir no banco, todas as vendas ficam SEM_NOTA
     * (o frontend exibirá "N/D – dados fiscais não disponíveis neste banco").
     */
    private function analiseFiscal(array $rows): array
    {
        $vendas   = [];
        $comNota  = ['qtd' => 0, 'total' => 0.0];
        $semNota  = ['qtd' => 0, 'total' => 0.0];
        $temCampoFiscal = false;

        foreach ($rows as $r) {
            $total   = round((float)($r['TOTAL'] ?? 0), 2);
            $nfeFlag = strtoupper(trim((string)($r['NFE_FLAG'] ?? '')));

            // Detecção fiscal — prioridade:
            // 1. TEM_NFCE='1' → NFC-e confirmada via NFCE_CABN (fonte mais confiável)
            // 2. NFE='S'      → NF-e modelo 55 autorizada
            $temNfce = ($r['TEM_NFCE'] ?? '0') === '1';
            $isNfe55 = $nfeFlag === 'S';
            $temDoc  = $temNfce || $isNfe55;

            if ($temDoc) $temCampoFiscal = true;

            $fiscal     = $temDoc ? 'COM_NOTA' : 'SEM_NOTA';
            $tipoFiscal = $temNfce ? 'NFC-e' : ($isNfe55 ? 'NF-e' : '');

            // Número/identificador fiscal para exibir
            $protNfce = trim((string)($r['PROTOCOLO_NFCE'] ?? ''));
            $nrFiscal = $protNfce ? substr($protNfce, -9) : ($temDoc ? $tipoFiscal : '—');

            $vendas[] = [
                'nr_venda'   => (string)($r['NR_VENDA'] ?? ''),
                'data_hora'  => $this->formatarDH($r['DATA_EMISSAO'], $r['HORA_EMISSAO'] ?? ''),
                'nr_fiscal'  => $temDoc ? (string)$nrFiscal : '—',
                'cliente'    => trim((string)($r['CLIENTE']   ?? 'Consumidor')) ?: 'Consumidor',
                'operador'   => trim((string)($r['OPERADOR']  ?? '')),
                'forma_pgto' => trim((string)($r['FORMA_PGTO'] ?? 'N/A')) ?: 'N/A',
                'total'      => $total,
                'fiscal'     => $fiscal,
            ];

            if ($temDoc) { $comNota['qtd']++; $comNota['total'] += $total; }
            else         { $semNota['qtd']++; $semNota['total'] += $total; }
        }

        $qtdTotal  = count($vendas);
        $totGeral  = $comNota['total'] + $semNota['total'];
        $percSem   = $qtdTotal > 0 ? round($semNota['qtd'] / $qtdTotal * 100, 1) : 0;
        $percCom   = $qtdTotal > 0 ? round($comNota['qtd'] / $qtdTotal * 100, 1) : 0;

        // Indicador de Risco Fiscal
        if ($percSem <= 5)       { $risco = 'Baixo';   $riscoCor = 'verde';   }
        elseif ($percSem <= 15)  { $risco = 'Atenção'; $riscoCor = 'amarelo'; }
        else                     { $risco = 'Crítico'; $riscoCor = 'vermelho';}

        return [
            'disponivel' => $temCampoFiscal || empty($rows),
            'resumo'     => [
                'qtd_com_nota'   => $comNota['qtd'],
                'qtd_sem_nota'   => $semNota['qtd'],
                'total_com_nota' => round($comNota['total'], 2),
                'total_sem_nota' => round($semNota['total'], 2),
                'total_geral'    => round($totGeral, 2),
                'perc_com_nota'  => $percCom,
                'perc_sem_nota'  => $percSem,
                'risco'          => $risco,
                'risco_cor'      => $riscoCor,
            ],
            'vendas' => $vendas,
        ];
    }

    // =========================================================================
    // HELPERS PÚBLICOS (usados pelo Controller da view)
    // =========================================================================

    public function getRepo(): FechamentoCaixaRepository
    {
        return $this->repo;
    }

    // =========================================================================
    // UTILITÁRIOS INTERNOS
    // =========================================================================

    /** Extrai 'YYYY-MM-DD' de qualquer formato retornado pelo PDO/Firebird */
    private function extrairData(mixed $val): string
    {
        if (!$val) return '';
        if ($val instanceof \DateTime) return $val->format('Y-m-d');
        return substr(trim((string)$val), 0, 10);
    }

    /** Formata data + hora como 'dd/mm/YYYY HH:MM' */
    private function formatarDH(mixed $data, mixed $hora): string
    {
        $d = $this->extrairData($data);
        if (!$d) return '—';
        $h = $hora ? substr(trim((string)$hora), 0, 5) : '';
        [$a, $m, $dia] = explode('-', $d);
        return "{$dia}/{$m}/{$a}" . ($h ? " {$h}" : '');
    }
}
