<?php
// Depto de Engenharia e Automação - Master Automação - Autor: Derlivon Silva
// (parte do Premium Express BI - Fire Sistemas)
//
// Tela interna de diagnostico: /diag/cadastro
// Raio-X da QUALIDADE do cadastro de produtos da loja ATIVA:
//   - Bloco TOPO: dados da conexao (host:port mascarados, ms, cod_empresa)
//   - 6 KPIs: total, % com preco, % com custo, % com marca, % com minimo, NOTA
//   - Alertas: prejuizo, custo suspeito, preco zero c/ estoque, custo total zero,
//     estoque negativo, cadastro velho (DT_ATUALIZACAO > 90d)
//   - Tabela detalhada com 6 filtros (todos/prejuizo/sem_preco/sem_custo/suspeito/sem_marca)
//   - Recomendacao final (texto curto explicando o que fazer)
//
// Padrao: copia a estrutura de DiagOperacoesController (try/catch, View::render,
// graceful degrade quando a conexao falha — view ainda mostra o bloco de config).

declare(strict_types=1);

namespace App\Controllers;

use App\Core\Database;
use App\Core\Lojas;
use App\Core\View;
use App\Repositories\EstoqueRepository;
use App\Repositories\ProdutosRepository;
use App\Repositories\SugestaoCompraRepository;

class DiagCadastroController
{
    /** Filtros validos da tabela detalhada. */
    private const FILTROS_VALIDOS = [
        'todos', 'prejuizo', 'sem_preco', 'sem_custo', 'suspeito', 'sem_marca'
    ];

    /** Limites permitidos pra tabela detalhada (whitelist).
     *  999999 = pseudo "Todos" — usa FIRST 999999 (Firebird aceita normal). */
    private const LIMITES_VALIDOS = [30, 100, 500, 2000, 999999];

    /** Devolve a config da loja ativa SEM senha — pra exibir no bloco TOPO. */
    private function configLojaSegura(?string $idLoja): array
    {
        // SINGLE-LOJA: le config/database.php direto
        if ($idLoja === null) {
            $path = dirname(__DIR__, 2) . '/config/database.php';
            $cfg  = is_file($path) ? (@include $path) : [];
            if (!is_array($cfg)) $cfg = [];
            return [
                'id'          => null,
                'nome'        => 'loja unica',
                'host'        => (string)($cfg['host']     ?? 'localhost'),
                'port'        => (int)   ($cfg['port']     ?? 3050),
                'database'    => (string)($cfg['database'] ?? ''),
                'cod_empresa' => (int)   ($cfg['cod_empresa'] ?? 0),
                'tipo'        => $this->detectaTipo((string)($cfg['host'] ?? 'localhost')),
            ];
        }
        $bruto = Lojas::get($idLoja) ?? [];
        $host  = (string)($bruto['host'] ?? 'localhost');
        return [
            'id'          => $idLoja,
            'nome'        => (string)($bruto['nome'] ?? $idLoja),
            'host'        => $host,
            'port'        => (int)   ($bruto['port']     ?? 3050),
            'database'    => (string)($bruto['database'] ?? ''),
            'cod_empresa' => (int)   ($bruto['cod_empresa'] ?? 0),
            'tipo'        => $this->detectaTipo($host),
        ];
    }

    private function detectaTipo(string $host): string
    {
        $h = strtolower(trim($host));
        if ($h === '' || $h === 'localhost' || $h === '127.0.0.1' || $h === '::1') {
            return 'local';
        }
        return 'remota';
    }

    /** Calcula a nota A/B/C/D do cadastro com base na media dos % de qualidade. */
    private function calcularNota(float $pctPreco, float $pctCusto, float $pctMarca, float $pctMinimo): string
    {
        $media = ($pctPreco + $pctCusto + $pctMarca + $pctMinimo) / 4;
        if ($media >= 90) return 'A';
        if ($media >= 75) return 'B';
        if ($media >= 60) return 'C';
        return 'D';
    }

    /** Texto WIN1252 -> UTF-8 (mesmo padrao dos repositorios). */
    private function utf8($s): string
    {
        $s = (string)$s;
        if ($s !== '' && !mb_check_encoding($s, 'UTF-8')) {
            $s = mb_convert_encoding($s, 'UTF-8', 'Windows-1252');
        }
        return $s;
    }

    /** % com cada flag de qualidade preenchida — usa ITENS_ESTOQUE da empresa ativa. */
    private function kpisQualidade(\PDO $pdo, int $codEmp): array
    {
        try {
            $sql = "SELECT
                COUNT(*) TOTAL,
                SUM(CASE WHEN ie.PRECO_VENDA  > 0           THEN 1 ELSE 0 END) COM_PRECO,
                SUM(CASE WHEN ie.CUSTO_FINAL  > 0           THEN 1 ELSE 0 END) COM_CUSTO,
                SUM(CASE WHEN i.COD_MARCA IS NOT NULL       THEN 1 ELSE 0 END) COM_MARCA,
                SUM(CASE WHEN i.ESTOQ_MINIMO > 0            THEN 1 ELSE 0 END) COM_MINIMO,
                SUM(CASE WHEN i.COD_ESTRUTURA IS NOT NULL   THEN 1 ELSE 0 END) COM_GRUPO,
                SUM(CASE WHEN i.NCM IS NOT NULL AND TRIM(i.NCM) <> '' THEN 1 ELSE 0 END) COM_NCM
            FROM ITENS_ESTOQUE ie
            JOIN ITENS i ON i.COD_ITEM = ie.COD_ITEM
            WHERE ie.COD_EMPRESA = {$codEmp} AND ie.QTD > 0";
            return $pdo->query($sql)->fetch(\PDO::FETCH_ASSOC) ?: [];
        } catch (\Throwable) {
            return [];
        }
    }

    /** Alertas extras nao cobertos pelos helpers existentes. */
    private function alertasExtras(\PDO $pdo, int $codEmp): array
    {
        $a = [
            'estoque_negativo'  => 0,
            'custo_zero_total'  => 0,
            'cadastro_velho'    => 0,
            'sem_grupo'         => 0,
            'custo_suspeito'    => 0,
        ];
        try {
            // estoque negativo
            $a['estoque_negativo'] = (int)$pdo->query(
                "SELECT COUNT(*) FROM ITENS_ESTOQUE ie
                 WHERE ie.COD_EMPRESA = {$codEmp} AND ie.QTD < 0"
            )->fetchColumn();

            // custo zero TOTAL (CUSTO_FINAL E CUSTO_COMPRA E CUSTO_MEDIO todos <=0)
            $a['custo_zero_total'] = (int)$pdo->query(
                "SELECT COUNT(*) FROM ITENS_ESTOQUE ie
                 WHERE ie.COD_EMPRESA = {$codEmp} AND ie.QTD > 0
                   AND COALESCE(ie.CUSTO_FINAL,0)  <= 0
                   AND COALESCE(ie.CUSTO_COMPRA,0) <= 0
                   AND COALESCE(ie.CUSTO_MEDIO,0)  <= 0"
            )->fetchColumn();

            // cadastro velho — DT_ATUALIZACAO > 90 dias (ou NULL)
            $a['cadastro_velho'] = (int)$pdo->query(
                "SELECT COUNT(*) FROM ITENS_ESTOQUE ie
                 WHERE ie.COD_EMPRESA = {$codEmp} AND ie.QTD > 0
                   AND (ie.DT_ATUALIZACAO IS NULL OR ie.DT_ATUALIZACAO < DATEADD(DAY,-90,CURRENT_DATE))"
            )->fetchColumn();

            // sem grupo/estrutura (agrupador comercial real do Fire)
            $a['sem_grupo'] = (int)$pdo->query(
                "SELECT COUNT(*) FROM ITENS_ESTOQUE ie
                 JOIN ITENS i ON i.COD_ITEM = ie.COD_ITEM
                 WHERE ie.COD_EMPRESA = {$codEmp} AND ie.QTD > 0
                   AND i.COD_ESTRUTURA IS NULL"
            )->fetchColumn();

            // custo suspeito (mesma regra do semMovimento: > 100k OR > 50x preco_venda)
            $a['custo_suspeito'] = (int)$pdo->query(
                "SELECT COUNT(*) FROM ITENS_ESTOQUE ie
                 WHERE ie.COD_EMPRESA = {$codEmp} AND ie.QTD > 0
                   AND ( ie.CUSTO_FINAL > 100000
                      OR (ie.PRECO_VENDA > 0 AND ie.CUSTO_FINAL > ie.PRECO_VENDA * 50) )"
            )->fetchColumn();
        } catch (\Throwable) {
            // graceful degrade — alertas que falharem viram 0
        }
        return $a;
    }

    /**
     * Top N problemas pra tabela detalhada (com busca opcional).
     *
     * @param string $busca texto livre ou COD_ITEM numerico
     * @return array linhas com colunas adicionais STATUS, MARKUP, CUSTO_COMPRA, CUSTO_MEDIO
     */
    private function problemas(\PDO $pdo, int $codEmp, string $filtro, int $limite = 30, string $busca = ''): array
    {
        $cond = '1=1';
        $ord  = 'CAST(ie.QTD AS DOUBLE PRECISION) * COALESCE(ie.CUSTO_FINAL,0) DESC';

        switch ($filtro) {
            case 'prejuizo':
                $cond = 'ie.PRECO_VENDA > 0 AND ie.CUSTO_FINAL > 0 AND ie.PRECO_VENDA < ie.CUSTO_FINAL';
                $ord  = '(ie.CUSTO_FINAL - ie.PRECO_VENDA) * CAST(ie.QTD AS DOUBLE PRECISION) DESC';
                break;
            case 'sem_preco':
                $cond = '(ie.PRECO_VENDA IS NULL OR ie.PRECO_VENDA <= 0)';
                break;
            case 'sem_custo':
                $cond = '(ie.CUSTO_FINAL IS NULL OR ie.CUSTO_FINAL <= 0)';
                break;
            case 'suspeito':
                $cond = '(ie.CUSTO_FINAL > 100000 OR (ie.PRECO_VENDA > 0 AND ie.CUSTO_FINAL > ie.PRECO_VENDA * 50))';
                $ord  = 'ie.CUSTO_FINAL DESC';
                break;
            case 'sem_marca':
                $cond = 'i.COD_MARCA IS NULL';
                break;
            case 'todos':
            default:
                // Mostra qualquer um dos problemas — uniao de criterios.
                $cond = "(
                    (ie.PRECO_VENDA IS NULL OR ie.PRECO_VENDA <= 0)
                 OR (ie.CUSTO_FINAL IS NULL OR ie.CUSTO_FINAL <= 0)
                 OR (ie.PRECO_VENDA > 0 AND ie.CUSTO_FINAL > 0 AND ie.PRECO_VENDA < ie.CUSTO_FINAL)
                 OR i.COD_MARCA IS NULL
                 OR (ie.CUSTO_FINAL > 100000 OR (ie.PRECO_VENDA > 0 AND ie.CUSTO_FINAL > ie.PRECO_VENDA * 50))
                )";
                break;
        }

        // Busca opcional — numero -> COD_ITEM, texto -> DESCRICAO LIKE
        $condBusca = '';
        $params    = [];
        $busca     = trim($busca);
        if ($busca !== '') {
            if (ctype_digit($busca)) {
                $condBusca = ' AND ie.COD_ITEM = :busca_cod';
                $params[':busca_cod'] = (int)$busca;
            } else {
                $condBusca = ' AND UPPER(i.DESCRICAO) LIKE UPPER(:busca_txt)';
                $params[':busca_txt'] = '%' . $busca . '%';
            }
        }

        $sql = "SELECT FIRST {$limite}
                ie.COD_ITEM,
                TRIM(i.DESCRICAO) DESCRICAO,
                TRIM(m.DESCRICAO) MARCA,
                COALESCE(ie.QTD,0)            QTD,
                COALESCE(ie.PRECO_VENDA,0)    PRECO_VENDA,
                COALESCE(ie.CUSTO_FINAL,0)    CUSTO_FINAL,
                COALESCE(ie.CUSTO_COMPRA,0)   CUSTO_COMPRA,
                COALESCE(ie.CUSTO_MEDIO,0)    CUSTO_MEDIO
            FROM ITENS_ESTOQUE ie
            JOIN ITENS i      ON i.COD_ITEM  = ie.COD_ITEM
            LEFT JOIN MARCA m ON m.COD_MARCA = i.COD_MARCA
            WHERE ie.COD_EMPRESA = {$codEmp} AND ie.QTD > 0
              AND ({$cond})
              {$condBusca}
            ORDER BY {$ord}";

        try {
            if ($params) {
                $stmt = $pdo->prepare($sql);
                $stmt->execute($params);
                $rows = $stmt->fetchAll(\PDO::FETCH_ASSOC);
            } else {
                $rows = $pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
            }
        } catch (\Throwable) {
            return [];
        }

        // Classifica status + markup; converte WIN1252.
        foreach ($rows as &$r) {
            $r['DESCRICAO'] = $this->utf8($r['DESCRICAO'] ?? '');
            $r['MARCA']     = $this->utf8($r['MARCA']     ?? '');
            $pv = (float)($r['PRECO_VENDA'] ?? 0);
            $cf = (float)($r['CUSTO_FINAL'] ?? 0);

            $status = [];
            if ($pv <= 0)                                                $status[] = 'sem_preco';
            if ($cf <= 0)                                                $status[] = 'sem_custo';
            if ($pv > 0 && $cf > 0 && $pv < $cf)                         $status[] = 'prejuizo';
            if ($cf > 100000 || ($pv > 0 && $cf > $pv * 50))             $status[] = 'suspeito';
            if (($r['MARCA'] ?? '') === '')                              $status[] = 'sem_marca';

            $r['STATUS']  = $status;
            $r['MARKUP']  = ($cf > 0 && $pv > 0) ? round((($pv - $cf) / $cf) * 100, 1) : null;
        }
        unset($r);

        return $rows;
    }

    /**
     * Conta o TOTAL de itens que casam com o filtro/busca (sem aplicar FIRST).
     * Usado pra mostrar "X de Y" na view e pra avisar quando precisa aumentar limite/exportar CSV.
     */
    private function problemasCount(\PDO $pdo, int $codEmp, string $filtro, string $busca = ''): int
    {
        $cond = '1=1';
        switch ($filtro) {
            case 'prejuizo':
                $cond = 'ie.PRECO_VENDA > 0 AND ie.CUSTO_FINAL > 0 AND ie.PRECO_VENDA < ie.CUSTO_FINAL';
                break;
            case 'sem_preco':
                $cond = '(ie.PRECO_VENDA IS NULL OR ie.PRECO_VENDA <= 0)';
                break;
            case 'sem_custo':
                $cond = '(ie.CUSTO_FINAL IS NULL OR ie.CUSTO_FINAL <= 0)';
                break;
            case 'suspeito':
                $cond = '(ie.CUSTO_FINAL > 100000 OR (ie.PRECO_VENDA > 0 AND ie.CUSTO_FINAL > ie.PRECO_VENDA * 50))';
                break;
            case 'sem_marca':
                $cond = 'i.COD_MARCA IS NULL';
                break;
            case 'todos':
            default:
                $cond = "(
                    (ie.PRECO_VENDA IS NULL OR ie.PRECO_VENDA <= 0)
                 OR (ie.CUSTO_FINAL IS NULL OR ie.CUSTO_FINAL <= 0)
                 OR (ie.PRECO_VENDA > 0 AND ie.CUSTO_FINAL > 0 AND ie.PRECO_VENDA < ie.CUSTO_FINAL)
                 OR i.COD_MARCA IS NULL
                 OR (ie.CUSTO_FINAL > 100000 OR (ie.PRECO_VENDA > 0 AND ie.CUSTO_FINAL > ie.PRECO_VENDA * 50))
                )";
                break;
        }

        $condBusca = '';
        $params    = [];
        $busca     = trim($busca);
        if ($busca !== '') {
            if (ctype_digit($busca)) {
                $condBusca = ' AND ie.COD_ITEM = :busca_cod';
                $params[':busca_cod'] = (int)$busca;
            } else {
                $condBusca = ' AND UPPER(i.DESCRICAO) LIKE UPPER(:busca_txt)';
                $params[':busca_txt'] = '%' . $busca . '%';
            }
        }

        $sql = "SELECT COUNT(*)
            FROM ITENS_ESTOQUE ie
            JOIN ITENS i      ON i.COD_ITEM  = ie.COD_ITEM
            LEFT JOIN MARCA m ON m.COD_MARCA = i.COD_MARCA
            WHERE ie.COD_EMPRESA = {$codEmp} AND ie.QTD > 0
              AND ({$cond})
              {$condBusca}";

        try {
            if ($params) {
                $stmt = $pdo->prepare($sql);
                $stmt->execute($params);
                return (int)$stmt->fetchColumn();
            }
            return (int)$pdo->query($sql)->fetchColumn();
        } catch (\Throwable) {
            return 0;
        }
    }

    /**
     * Exporta o conjunto $detalhe como CSV BR (separador ;, BOM UTF-8) e encerra a request.
     * Nome: diag-cadastro-{loja}-{filtro}-{YYYYMMDDHHmm}.csv
     */
    private function exportarCsv(array $detalhe, string $loja, string $filtro): void
    {
        $slug = preg_replace('/[^a-z0-9]+/i', '-', strtolower($loja !== '' ? $loja : 'loja'));
        $slug = trim((string)$slug, '-') ?: 'loja';
        $nome = sprintf('diag-cadastro-%s-%s-%s.csv', $slug, $filtro, date('YmdHi'));

        // Limpa qualquer buffer pra nao corromper o CSV.
        while (ob_get_level() > 0) { @ob_end_clean(); }

        header('Content-Type: text/csv; charset=UTF-8');
        header('Content-Disposition: attachment; filename="' . $nome . '"');
        header('Cache-Control: no-store, no-cache, must-revalidate');
        header('Pragma: no-cache');

        // BOM UTF-8 pra Excel BR abrir como UTF-8 (acentos preservados).
        echo chr(0xEF) . chr(0xBB) . chr(0xBF);

        $fmtDec = static fn($v) => number_format((float)$v, 2, ',', '');
        $fmtPct = static fn($v) => $v === null ? '' : number_format((float)$v, 1, ',', '');

        $out = fopen('php://output', 'w');
        // Header
        fputcsv($out, [
            'COD_ITEM','DESCRICAO','MARCA','ESTOQUE',
            'PRECO_VENDA','CUSTO_FINAL','CUSTO_COMPRA','CUSTO_MEDIO',
            'MARKUP_PCT','STATUS'
        ], ';');

        foreach ($detalhe as $r) {
            $status = $r['STATUS'] ?? [];
            if (is_array($status)) {
                $status = $status === [] ? 'ok' : implode('|', $status);
            }
            fputcsv($out, [
                (string)($r['COD_ITEM']     ?? ''),
                (string)($r['DESCRICAO']    ?? ''),
                (string)($r['MARCA']        ?? ''),
                $fmtDec($r['QTD']           ?? 0),
                $fmtDec($r['PRECO_VENDA']   ?? 0),
                $fmtDec($r['CUSTO_FINAL']   ?? 0),
                $fmtDec($r['CUSTO_COMPRA']  ?? 0),
                $fmtDec($r['CUSTO_MEDIO']   ?? 0),
                $fmtPct($r['MARKUP']        ?? null),
                (string)$status,
            ], ';');
        }
        fclose($out);
        exit;
    }

    /** Top 5 itens com MAIOR estoque (R$) E sem custo cadastrado — pra recomendacao. */
    private function topSemCusto(\PDO $pdo, int $codEmp, int $limite = 5): array
    {
        try {
            // Sem custo final -> ranqueia por PRECO_VENDA * QTD (proxy do tamanho do gap).
            $sql = "SELECT FIRST {$limite}
                    ie.COD_ITEM,
                    TRIM(i.DESCRICAO) DESCRICAO,
                    COALESCE(ie.QTD,0)         QTD,
                    COALESCE(ie.PRECO_VENDA,0) PRECO_VENDA,
                    CAST(ie.QTD AS DOUBLE PRECISION) * COALESCE(ie.PRECO_VENDA,0) PESO
                FROM ITENS_ESTOQUE ie
                JOIN ITENS i ON i.COD_ITEM = ie.COD_ITEM
                WHERE ie.COD_EMPRESA = {$codEmp}
                  AND ie.QTD > 0
                  AND (ie.CUSTO_FINAL IS NULL OR ie.CUSTO_FINAL <= 0)
                ORDER BY CAST(ie.QTD AS DOUBLE PRECISION) * COALESCE(ie.PRECO_VENDA,0) DESC";
            $rows = $pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
            foreach ($rows as &$r) { $r['DESCRICAO'] = $this->utf8($r['DESCRICAO'] ?? ''); }
            unset($r);
            return $rows;
        } catch (\Throwable) {
            return [];
        }
    }

    public function index(): void
    {
        // Loja remota via VPN pode ser lenta — folga.
        @ini_set('memory_limit', '512M');
        @set_time_limit(120);

        $filtro = (string)($_GET['f'] ?? 'todos');
        if (!in_array($filtro, self::FILTROS_VALIDOS, true)) {
            $filtro = 'todos';
        }

        // Limite via whitelist (default 30, max 2000)
        $limiteIn = (int)($_GET['limite'] ?? 30);
        $limite   = in_array($limiteIn, self::LIMITES_VALIDOS, true) ? $limiteIn : 30;

        // Busca livre — sanitiza (tira controle, limita tamanho)
        $busca = trim((string)($_GET['busca'] ?? ''));
        if ($busca !== '') {
            $busca = preg_replace('/[\x00-\x1F\x7F]/u', '', $busca) ?? '';
            if (mb_strlen($busca) > 80) $busca = mb_substr($busca, 0, 80);
        }

        // Export CSV?
        $csv = !empty($_GET['csv']);

        $erro             = '';
        $msConexao        = null;
        $msQuery          = null;
        $statusConexao    = false;
        $msgConexao       = '';
        $codEmp           = 0;
        $catalogoKpis     = [];
        $saudeKpis        = [];
        $estoqueKpis      = [];
        $minimosKpis      = [];
        $rupturaCount     = 0;
        $qualidade        = [];
        $alertasExtras    = [];
        $semMovimento     = [];
        $listas           = [];
        $detalhe          = [];
        $detalheTotal     = 0;
        $topSemCusto      = [];
        $idLoja           = null;
        $cfgLoja          = [];

        try {
            $idLoja  = Lojas::idAtiva();
        } catch (\Throwable) { $idLoja = null; }

        $cfgLoja = $this->configLojaSegura($idLoja);

        // ── Conexao + cronometro ───────────────────────────────────────────
        try {
            $t0  = microtime(true);
            $pdo = $idLoja !== null ? Database::conectarLoja($idLoja) : Database::conectar();
            $msConexao = (int) round((microtime(true) - $t0) * 1000);

            // Query de sanity check
            $t1 = microtime(true);
            $pdo->query('SELECT 1 FROM RDB$DATABASE')->fetchColumn();
            $msQuery = (int) round((microtime(true) - $t1) * 1000);

            $statusConexao = true;
            $msgConexao    = 'OK';

            $codEmp = (int) Lojas::codEmpresaAtiva();

            // ── Cada bloco em try/catch isolado (graceful degrade) ──────────
            try { $catalogoKpis  = (new ProdutosRepository($pdo))->catalogoKpis();       } catch (\Throwable $e) { $catalogoKpis  = ['_erro' => $e->getMessage()]; }
            try { $saudeKpis     = (new ProdutosRepository($pdo))->saudeKpis();          } catch (\Throwable $e) { $saudeKpis     = ['_erro' => $e->getMessage()]; }
            try { $estoqueKpis   = (new EstoqueRepository($pdo))->kpis();                } catch (\Throwable $e) { $estoqueKpis   = ['_erro' => $e->getMessage()]; }
            try { $minimosKpis   = (new EstoqueRepository($pdo))->kpisMinimos();         } catch (\Throwable $e) { $minimosKpis   = ['_erro' => $e->getMessage()]; }
            try { $rupturaCount  = (new SugestaoCompraRepository($pdo, $codEmp))->rupturaCount(); } catch (\Throwable) { $rupturaCount = 0; }

            $qualidade     = $this->kpisQualidade($pdo, $codEmp);
            $alertasExtras = $this->alertasExtras($pdo, $codEmp);

            // Custo suspeito reaproveita o semMovimento (que ja marca CUSTO_SUSPEITO)
            try {
                $semMov = (new ProdutosRepository($pdo))->semMovimento(date('Y-m-d'), 90, 200);
                $semMovimento = array_values(array_filter($semMov, fn($r) => !empty($r['CUSTO_SUSPEITO'])));
            } catch (\Throwable) { $semMovimento = []; }

            // Listas (limite 50 cada) das 6 categorias de problema
            $tiposLista = ['sem_custo','sem_preco','prejuizo','sem_ncm','sem_foto','sem_barras'];
            foreach ($tiposLista as $tipo) {
                try {
                    $listas[$tipo] = (new ProdutosRepository($pdo))->saudeLista($tipo, 50);
                } catch (\Throwable) {
                    $listas[$tipo] = [];
                }
            }

            // Tabela detalhada (top N do filtro corrente + busca opcional)
            $detalhe       = $this->problemas($pdo, $codEmp, $filtro, $limite, $busca);
            $detalheTotal  = $this->problemasCount($pdo, $codEmp, $filtro, $busca);

            // ── Cadastro fiscal (PDF Cadastro Fiscal por Produto) ─────────────
            // FIX 25/06/2026: NAO carregamos mais o dump fiscal aqui — bloqueava
            // a tela com timeout em loja grande/VPN. Agora a view chama o
            // endpoint /diag/cadastro/fiscal-json sob demanda (so quando o user
            // clica "Cadastro Fiscal (PDF)" no FAB). Veja fiscalJson() abaixo.

            // Export CSV — encerra a request aqui (sem renderizar a view).
            if ($csv) {
                // Pra CSV, exporta exatamente o que esta na tabela (limite + busca aplicados).
                $this->exportarCsv($detalhe, (string)($cfgLoja['nome'] ?? 'loja'), $filtro);
                return; // unreachable: exportarCsv() chama exit
            }

            // Recomendacao final
            $topSemCusto = $this->topSemCusto($pdo, $codEmp, 5);

        } catch (\Throwable $e) {
            $erro          = $e->getMessage();
            $statusConexao = false;
            $msgConexao    = $e->getMessage();
        }

        // ── Calculo dos % de qualidade + nota ─────────────────────────────────
        $total      = (int) ($qualidade['TOTAL']      ?? 0);
        $comPreco   = (int) ($qualidade['COM_PRECO']  ?? 0);
        $comCusto   = (int) ($qualidade['COM_CUSTO']  ?? 0);
        $comMarca   = (int) ($qualidade['COM_MARCA']  ?? 0);
        $comMinimo  = (int) ($qualidade['COM_MINIMO'] ?? 0);
        $comGrupo   = (int) ($qualidade['COM_GRUPO']  ?? 0);
        $comNcm     = (int) ($qualidade['COM_NCM']    ?? 0);

        $pctPreco  = $total > 0 ? round($comPreco  / $total * 100, 1) : 0.0;
        $pctCusto  = $total > 0 ? round($comCusto  / $total * 100, 1) : 0.0;
        $pctMarca  = $total > 0 ? round($comMarca  / $total * 100, 1) : 0.0;
        $pctMinimo = $total > 0 ? round($comMinimo / $total * 100, 1) : 0.0;
        $pctGrupo  = $total > 0 ? round($comGrupo  / $total * 100, 1) : 0.0;
        $pctNcm    = $total > 0 ? round($comNcm    / $total * 100, 1) : 0.0;

        $nota = $this->calcularNota($pctPreco, $pctCusto, $pctMarca, $pctMinimo);

        // Texto curto de recomendacao
        $diagnosticos = [];
        if ($pctCusto  < 90) $diagnosticos[] = sprintf('%.1f%% sem custo cadastrado', 100 - $pctCusto);
        if ($pctPreco  < 90) $diagnosticos[] = sprintf('%.1f%% sem preco de venda',   100 - $pctPreco);
        if ($pctMarca  < 70) $diagnosticos[] = sprintf('%.1f%% sem marca',            100 - $pctMarca);
        if ($pctMinimo < 50) $diagnosticos[] = sprintf('%.1f%% sem estoque minimo',   100 - $pctMinimo);
        if (($alertasExtras['custo_suspeito'] ?? 0) > 0) $diagnosticos[] = $alertasExtras['custo_suspeito'] . ' com custo suspeito';
        if ((int)($saudeKpis['PREJUIZO'] ?? 0) > 0)      $diagnosticos[] = (int)$saudeKpis['PREJUIZO'] . ' vendendo no prejuizo';

        View::render('diag/cadastro', [
            'titulo'        => 'Raio-X do Cadastro — ' . ($cfgLoja['nome'] ?? 'loja'),
            'subtitulo'     => 'Qualidade do cadastro de produtos: preco, custo, marca, grupo, estoque',
            'erro'          => $erro,
            // bloco TOPO
            'cfg_loja'      => $cfgLoja,
            'status_conexao'=> $statusConexao,
            'msg_conexao'   => $msgConexao,
            'ms_conexao'    => $msConexao,
            'ms_query'      => $msQuery,
            'cod_empresa'   => $codEmp,
            // KPIs principais
            'total'         => $total,
            'pct_preco'     => $pctPreco,
            'pct_custo'     => $pctCusto,
            'pct_marca'     => $pctMarca,
            'pct_minimo'    => $pctMinimo,
            'pct_grupo'     => $pctGrupo,
            'pct_ncm'       => $pctNcm,
            'nota'          => $nota,
            // KPIs auxiliares (catalogo + estoque)
            'catalogo_kpis' => $catalogoKpis,
            'saude_kpis'    => $saudeKpis,
            'estoque_kpis'  => $estoqueKpis,
            'minimos_kpis'  => $minimosKpis,
            'ruptura_count' => $rupturaCount,
            // alertas extras (estoque negativo, custo zero total, cadastro velho, sem grupo, custo suspeito)
            'alertas_extras'=> $alertasExtras,
            'sem_movimento_suspeitos' => $semMovimento,
            // listas das 6 categorias (cards colapsaveis)
            'listas'        => $listas,
            // tabela detalhada
            'filtro'        => $filtro,
            'filtros_validos'=> self::FILTROS_VALIDOS,
            'limite'        => $limite,
            'limites_validos'=> self::LIMITES_VALIDOS,
            'busca'         => $busca,
            'detalhe'       => $detalhe,
            'detalhe_total' => $detalheTotal,
            // recomendacao
            'diagnosticos'  => $diagnosticos,
            'top_sem_custo' => $topSemCusto,
        ]);
    }

    /**
     * Endpoint JSON do cadastro fiscal — alimenta o PDF "Cadastro Fiscal por
     * Produto" sob demanda (FAB → gerarPDFFiscal()).
     *
     * Antes (ate 25/06/2026) o dump fiscal vinha embutido na view via
     * <script id="fiscal-data">. Carregava 1458+ itens em TODA carga da tela,
     * causando HTTP 500 (timeout 120s na VPN). Agora a view so chama este
     * endpoint quando o usuario realmente pede o PDF.
     */
    public function fiscalJson(): void
    {
        @set_time_limit(180);
        @ini_set('memory_limit', '512M');
        while (ob_get_level() > 0) { ob_end_clean(); }
        header('Content-Type: application/json; charset=UTF-8');
        try {
            $idLoja = \App\Core\Lojas::idAtiva();
            $pdo = $idLoja !== null ? \App\Core\Database::conectarLoja($idLoja) : \App\Core\Database::conectar();
            $produtos = new \App\Repositories\ProdutosRepository($pdo);
            $lista = $produtos->cadastroFiscal(999999, '');
            echo json_encode([
                'ok' => true,
                'loja' => $idLoja !== null ? (\App\Core\Lojas::get($idLoja)['nome'] ?? $idLoja) : 'loja unica',
                'cod_empresa' => \App\Core\Lojas::codEmpresaAtiva(),
                'itens' => $lista,
                'total' => count($lista),
                'data' => date('Y-m-d H:i:s'),
            ]);
        } catch (\Throwable $e) {
            echo json_encode(['ok' => false, 'erro' => $e->getMessage()]);
        }
        exit;
    }
}
