<?php

namespace App\Repositories;

use App\Core\Cache;
use App\Core\Lojas;

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

    /** Empresa (loja) ativa — filtra vendas p/ não misturar lojas no mesmo banco. */
    private function empresa(): int
    {
        try { $e = (int) Lojas::codEmpresaAtiva(); return $e ?: 5; }
        catch (\Throwable) { return 5; }
    }

    /** Texto do banco é WIN1252 → UTF-8 (senão json_encode esvazia em descrições acentuadas). */
    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;
    }

    public function kpis(string $dataIni, string $dataFim): array
    {
        // Exclui canceladas (COD_SITUACAO=3) e fixa a empresa da loja.
        $emp = $this->empresa();
        $sql = "SELECT
            COUNT(DISTINCT ni.COD_ITEM) PRODUTOS_VENDIDOS,
            SUM(ni.QTD) QTD_TOTAL,
            SUM(ni.QTD * ni.PRECO_UNIT) FATURAMENTO
        FROM NOTAS_ITENS ni
        JOIN NOTAS_CAB nc ON nc.NR_NOTA = ni.NR_NOTA
                          AND nc.COD_TIPO_OPERACAO = ni.COD_TIPO_OPERACAO
        WHERE ni.COD_TIPO_OPERACAO IN (115,231,297)
          AND nc.COD_SITUACAO <> 3
          AND nc.COD_EMPRESA = {$emp}
          AND ni.DATA_EMISSAO BETWEEN ? AND ?";
        $st = $this->pdo->prepare($sql);
        $st->execute([$dataIni, $dataFim]);
        return $st->fetch(\PDO::FETCH_ASSOC) ?: [];
    }

    public function curvaAbc(string $dataIni, string $dataFim, int $limite = 1000): array
    {
        $ttl = ($dataFim >= date('Y-m-d')) ? 300 : 86400;
        $emp = $this->empresa();
        return Cache::lembrar("produtos_curva_abc_{$emp}_{$dataIni}_{$dataFim}_{$limite}", $ttl, function () use ($dataIni, $dataFim, $limite, $emp) {
            // Exclui canceladas (COD_SITUACAO=3) e fixa a empresa da loja.
            $sql = "SELECT FIRST {$limite}
                ni.COD_ITEM,
                i.DESCRICAO,
                i.COD_UNIDADE,
                SUM(ni.QTD) QTD_TOTAL,
                SUM(ni.QTD * ni.PRECO_UNIT) FATURAMENTO,
                SUM(ni.QTD * ni.CUSTO_FINAL) CMV,
                AVG(ni.PRECO_UNIT) PRECO_MEDIO,
                COUNT(DISTINCT ni.NR_NOTA) QTD_NOTAS
            FROM NOTAS_ITENS ni
            JOIN ITENS i      ON ni.COD_ITEM = i.COD_ITEM
            JOIN NOTAS_CAB nc ON nc.NR_NOTA = ni.NR_NOTA
                              AND nc.COD_TIPO_OPERACAO = ni.COD_TIPO_OPERACAO
            WHERE ni.COD_TIPO_OPERACAO IN (115,231,297)
              AND nc.COD_SITUACAO <> 3
              AND nc.COD_EMPRESA = {$emp}
              AND ni.DATA_EMISSAO BETWEEN ? AND ?
            GROUP BY ni.COD_ITEM, i.DESCRICAO, i.COD_UNIDADE
            ORDER BY FATURAMENTO DESC";
            $st = $this->pdo->prepare($sql);
            $st->execute([$dataIni, $dataFim]);
            $rows = $st->fetchAll(\PDO::FETCH_ASSOC);

            $totalFat = array_sum(array_column($rows, 'FATURAMENTO'));
            $acum = 0;
            foreach ($rows as &$r) {
                $r['DESCRICAO']   = $this->utf8($r['DESCRICAO']   ?? '');
                $r['COD_UNIDADE'] = $this->utf8($r['COD_UNIDADE'] ?? '');
                $acum += $r['FATURAMENTO'];
                $perc = $totalFat > 0 ? ($r['FATURAMENTO'] / $totalFat * 100) : 0;
                $percAcum = $totalFat > 0 ? ($acum / $totalFat * 100) : 0;
                $r['PERC_FAT']  = round($perc, 2);
                $r['PERC_ACUM'] = round($percAcum, 2);
                $r['CLASSE'] = $percAcum <= 70 ? 'A' : ($percAcum <= 90 ? 'B' : 'C');
                // Lucro (faturamento − CMV) e margem %, p/ ranquear por LUCRO/margem
                $fat = (float)$r['FATURAMENTO'];
                $cmv = (float)($r['CMV'] ?? 0);
                $r['LUCRO']       = round($fat - $cmv, 2);
                $r['MARGEM_PERC'] = $fat > 0 ? round(($fat - $cmv) / $fat * 100, 1) : 0;
            }
            unset($r);
            return $rows;
        });
    }

    public function semMovimento(string $dataRef, int $diasSemVenda = 90, int $limite = 200): array
    {
        $emp = $this->empresa();
        return Cache::lembrar("produtos_sem_mov_{$emp}_{$dataRef}_{$diasSemVenda}_{$limite}", 3600, function () use ($dataRef, $diasSemVenda, $limite, $emp) {
            $dataCorte = date('Y-m-d', strtotime($dataRef . " -{$diasSemVenda} days"));
            $sql = "SELECT FIRST {$limite}
                ie.COD_ITEM,
                i.DESCRICAO,
                i.COD_UNIDADE,
                ie.QTD ESTOQUE_ATUAL,
                ie.PRECO_VENDA,
                ie.CUSTO_FINAL,
                CAST(ie.QTD AS DOUBLE PRECISION) * ie.CUSTO_FINAL VALOR_PARADO,
                ie.DT_ATUALIZACAO,
                sm.ULTIMA_VENDA
            FROM ITENS_ESTOQUE ie
            JOIN ITENS i ON ie.COD_ITEM = i.COD_ITEM
            LEFT JOIN (
                SELECT COD_ITEM, MAX(DATA_EMISSAO) ULTIMA_VENDA
                FROM NOTAS_ITENS
                WHERE COD_TIPO_OPERACAO IN (115,231,297)
                  AND COD_EMPRESA = {$emp}
                GROUP BY COD_ITEM
            ) sm ON sm.COD_ITEM = ie.COD_ITEM
            WHERE ie.COD_EMPRESA = {$emp} AND ie.QTD > 0
              AND (sm.ULTIMA_VENDA IS NULL OR sm.ULTIMA_VENDA < ?)
            ORDER BY CAST(ie.QTD AS DOUBLE PRECISION) * ie.CUSTO_FINAL DESC";
            $st = $this->pdo->prepare($sql);
            $st->execute([$dataCorte]);
            $rows = $st->fetchAll(\PDO::FETCH_ASSOC);
            foreach ($rows as &$r) {            // WIN1252 → UTF-8 (senão json_encode esvazia)
                $r['DESCRICAO']   = $this->utf8($r['DESCRICAO']   ?? '');
                $r['COD_UNIDADE'] = $this->utf8($r['COD_UNIDADE'] ?? '');
                // Custo provavelmente errado no cadastro (ex.: R$ 7,8 tri num boxer):
                // absurdo (> R$ 100 mil) ou muito acima do preço de venda (> 50x).
                $custo = (float)($r['CUSTO_FINAL'] ?? 0); $pv = (float)($r['PRECO_VENDA'] ?? 0);
                $r['CUSTO_SUSPEITO'] = ($custo > 100000) || ($pv > 0 && $custo > $pv * 50);
            }
            unset($r);
            return $rows;
        });
    }

    public function historicoPrecos(string $busca, int $limite = 20): array
    {
        if (!$busca) return [];
        $emp = $this->empresa();
        $sql = "SELECT FIRST {$limite}
            i.COD_ITEM, i.DESCRICAO,
            ie.PRECO_VENDA,
            ie.PRECO_ANT1, ie.DT_ANT1,
            ie.PRECO_ANT2, ie.DT_ANT2,
            ie.PRECO_ANT3, ie.DT_ANT3,
            ie.CUSTO_FINAL, ie.CUSTO_MEDIO,
            ie.DT_ATUALIZACAO
        FROM ITENS_ESTOQUE ie
        JOIN ITENS i ON ie.COD_ITEM = i.COD_ITEM
        WHERE ie.COD_EMPRESA = {$emp} AND UPPER(i.DESCRICAO) CONTAINING UPPER(?)
        ORDER BY i.DESCRICAO";
        $st = $this->pdo->prepare($sql);
        $st->execute([$busca]);
        $rows = $st->fetchAll(\PDO::FETCH_ASSOC);
        foreach ($rows as &$r) { $r['DESCRICAO'] = $this->utf8($r['DESCRICAO'] ?? ''); }
        unset($r);
        return $rows;
    }

    public function topProdutos(string $dataIni, string $dataFim, int $limite = 10): array
    {
        $emp = $this->empresa();
        $sql = "SELECT FIRST {$limite}
            i.DESCRICAO,
            SUM(ni.QTD) QTD,
            SUM(ni.QTD * ni.PRECO_UNIT) FATURAMENTO
        FROM NOTAS_ITENS ni JOIN ITENS i ON ni.COD_ITEM = i.COD_ITEM
        WHERE ni.COD_TIPO_OPERACAO IN (115,231,297)
          AND ni.COD_EMPRESA = {$emp}
          AND ni.DATA_EMISSAO BETWEEN ? AND ?
        GROUP BY i.DESCRICAO
        ORDER BY FATURAMENTO DESC";
        $st = $this->pdo->prepare($sql);
        $st->execute([$dataIni, $dataFim]);
        return $st->fetchAll(\PDO::FETCH_ASSOC);
    }

    /** Ranking de produtos por QUANTIDADE vendida no periodo (global; usa filtroVendaSql do VendasRepository, respeitando overrides por cliente). */
    public function topPorQuantidade(string $dataIni, string $dataFim, int $limite = 20): array
    {
        $ttl = ($dataFim >= date('Y-m-d')) ? 60 : 1800;
        return Cache::lembrar("produtos_top_qtd_{$dataIni}_{$dataFim}_{$limite}", $ttl, function () use ($dataIni, $dataFim, $limite) {
            $vr = new VendasRepository($this->pdo);
            $fv = $vr->filtroVendaSql('nc');
            $fe = $vr->filtroEmpresaSql('nc');
            $sql = "SELECT FIRST {$limite}
                i.COD_ITEM,
                i.DESCRICAO,
                SUM(ni.QTD)                       QTD_TOTAL,
                SUM(ni.QTD * ni.PRECO_UNIT)       FATURAMENTO,
                COUNT(DISTINCT nc.NR_NOTA)        QTD_NOTAS
            FROM NOTAS_ITENS ni
            JOIN NOTAS_CAB nc ON nc.NR_NOTA = ni.NR_NOTA
                              AND nc.COD_TIPO_OPERACAO = ni.COD_TIPO_OPERACAO
                              AND nc.COD_EMPRESA = ni.COD_EMPRESA
            JOIN ITENS i ON ni.COD_ITEM = i.COD_ITEM
            WHERE nc.DATA_EMISSAO BETWEEN ? AND ?
              AND nc.COD_SITUACAO <> 3
              {$fv}
              {$fe}
            GROUP BY i.COD_ITEM, i.DESCRICAO
            ORDER BY QTD_TOTAL DESC";
            $st = $this->pdo->prepare($sql);
            $st->execute([$dataIni, $dataFim]);
            $rows = $st->fetchAll(\PDO::FETCH_ASSOC);
            foreach ($rows as &$r) { $r['DESCRICAO'] = $this->utf8($r['DESCRICAO'] ?? ''); }
            return $rows;
        });
    }

    /** Ranking de produtos por FATURAMENTO no periodo. Mesma logica do topPorQuantidade mas ordena por valor. */
    public function topPorFaturamento(string $dataIni, string $dataFim, int $limite = 20): array
    {
        $ttl = ($dataFim >= date('Y-m-d')) ? 60 : 1800;
        return Cache::lembrar("produtos_top_fat_{$dataIni}_{$dataFim}_{$limite}", $ttl, function () use ($dataIni, $dataFim, $limite) {
            $vr = new VendasRepository($this->pdo);
            $fv = $vr->filtroVendaSql('nc');
            $fe = $vr->filtroEmpresaSql('nc');
            $sql = "SELECT FIRST {$limite}
                i.COD_ITEM,
                i.DESCRICAO,
                SUM(ni.QTD)                       QTD_TOTAL,
                SUM(ni.QTD * ni.PRECO_UNIT)       FATURAMENTO,
                COUNT(DISTINCT nc.NR_NOTA)        QTD_NOTAS
            FROM NOTAS_ITENS ni
            JOIN NOTAS_CAB nc ON nc.NR_NOTA = ni.NR_NOTA
                              AND nc.COD_TIPO_OPERACAO = ni.COD_TIPO_OPERACAO
                              AND nc.COD_EMPRESA = ni.COD_EMPRESA
            JOIN ITENS i ON ni.COD_ITEM = i.COD_ITEM
            WHERE nc.DATA_EMISSAO BETWEEN ? AND ?
              AND nc.COD_SITUACAO <> 3
              {$fv}
              {$fe}
            GROUP BY i.COD_ITEM, i.DESCRICAO
            ORDER BY FATURAMENTO DESC";
            $st = $this->pdo->prepare($sql);
            $st->execute([$dataIni, $dataFim]);
            $rows = $st->fetchAll(\PDO::FETCH_ASSOC);
            foreach ($rows as &$r) { $r['DESCRICAO'] = $this->utf8($r['DESCRICAO'] ?? ''); }
            return $rows;
        });
    }

    /** Produtos com MARGEM NEGATIVA no periodo — preco medio de venda < custo do cadastro (CUSTO_MEDIO ou CUSTO_COMPRA).
     *  Retorna ['sem_custo'=>true, ...] se NENHUM produto vendido tiver custo cadastrado (caso bar/restaurante). */
    public function margemNegativa(string $dataIni, string $dataFim, int $limite = 30): array
    {
        $ttl = ($dataFim >= date('Y-m-d')) ? 60 : 1800;
        return Cache::lembrar("produtos_margem_neg_v2_{$dataIni}_{$dataFim}_{$limite}", $ttl, function () use ($dataIni, $dataFim, $limite) {
            $emp = $this->empresa();
            // Sanity: existe ALGUM produto vendido no periodo com custo > 0?
            $vr = new VendasRepository($this->pdo);
            $fvSan = $vr->filtroVendaSql('nc');
            $feSan = $vr->filtroEmpresaSql('nc');
            $stSan = $this->pdo->prepare("
                SELECT COUNT(DISTINCT i.COD_ITEM) C
                FROM NOTAS_ITENS ni
                JOIN NOTAS_CAB nc ON nc.NR_NOTA = ni.NR_NOTA
                                  AND nc.COD_TIPO_OPERACAO = ni.COD_TIPO_OPERACAO
                                  AND nc.COD_EMPRESA = ni.COD_EMPRESA
                JOIN ITENS i ON ni.COD_ITEM = i.COD_ITEM
                WHERE nc.DATA_EMISSAO BETWEEN ? AND ?
                  AND nc.COD_SITUACAO <> 3
                  AND COALESCE(NULLIF(i.CUSTO_MEDIO,0), i.CUSTO_COMPRA) > 0
                  {$fvSan} {$feSan}
            ");
            $stSan->execute([$dataIni, $dataFim]);
            $qtdComCusto = (int)$stSan->fetchColumn();
            if ($qtdComCusto === 0) {
                return ['sem_custo' => true, 'rows' => []];
            }

            $vr = new VendasRepository($this->pdo);
            $fv = $vr->filtroVendaSql('nc');
            $fe = $vr->filtroEmpresaSql('nc');
            // Selecao bruta (sem FIRST + sem ORDER BY de subquery) -> ordena/limita na query externa.
            // Custo: prefere CUSTO_MEDIO; cai p/ CUSTO_COMPRA se CUSTO_MEDIO for null/zero.
            // Colunas reais do ERP Fire (Real Prime): NAO existe i.PRECO_CUSTO.
            $custoExpr = "COALESCE(NULLIF(i.CUSTO_MEDIO, 0), i.CUSTO_COMPRA)";
            $sql = "SELECT FIRST {$limite} *
            FROM (
                SELECT
                    i.COD_ITEM,
                    i.DESCRICAO,
                    {$custoExpr}                                 AS CUSTO_UNIT,
                    SUM(ni.QTD)                                  AS QTD_VENDIDA,
                    SUM(ni.QTD * ni.PRECO_UNIT)                  AS FATURAMENTO,
                    SUM(ni.QTD * ni.PRECO_UNIT) / SUM(ni.QTD)    AS PRECO_MEDIO,
                    (SUM(ni.QTD * ni.PRECO_UNIT) - SUM(ni.QTD) * {$custoExpr}) AS LUCRO_PERIODO,
                    ((SUM(ni.QTD * ni.PRECO_UNIT)/SUM(ni.QTD)) - {$custoExpr}) AS MARGEM_UNIT
                FROM NOTAS_ITENS ni
                JOIN NOTAS_CAB nc ON nc.NR_NOTA = ni.NR_NOTA
                                  AND nc.COD_TIPO_OPERACAO = ni.COD_TIPO_OPERACAO
                                  AND nc.COD_EMPRESA = ni.COD_EMPRESA
                JOIN ITENS i ON ni.COD_ITEM = i.COD_ITEM
                WHERE nc.DATA_EMISSAO BETWEEN ? AND ?
                  AND nc.COD_SITUACAO <> 3
                  AND {$custoExpr} > 0
                  {$fv}
                  {$fe}
                GROUP BY i.COD_ITEM, i.DESCRICAO, i.CUSTO_MEDIO, i.CUSTO_COMPRA
                HAVING SUM(ni.QTD) > 0
                   AND (SUM(ni.QTD * ni.PRECO_UNIT)/SUM(ni.QTD)) < {$custoExpr}
            ) x
            ORDER BY LUCRO_PERIODO ASC";
            $st = $this->pdo->prepare($sql);
            $st->execute([$dataIni, $dataFim]);
            $rows = $st->fetchAll(\PDO::FETCH_ASSOC);
            foreach ($rows as &$r) { $r['DESCRICAO'] = $this->utf8($r['DESCRICAO'] ?? ''); }
            return ['sem_custo' => false, 'rows' => $rows];
        });
    }

    public function ultimaData(): string
    {
        $emp = $this->empresa();
        try {
            // Janela recente (120d) — MAX sem limite custa ~45s no Firebird 2.5 sem índice descendente.
            $r = $this->pdo->query("SELECT MAX(DATA_EMISSAO) D FROM NOTAS_CAB WHERE COD_TIPO_OPERACAO IN (115,231,297) AND COD_SITUACAO <> 3 AND COD_EMPRESA = {$emp} AND DATA_EMISSAO >= DATEADD(DAY,-120,CURRENT_DATE) AND DATA_EMISSAO <= CURRENT_DATE")->fetchColumn();
            if (!$r) return date('Y-m-d');
            if ($r instanceof \DateTime) return $r->format('Y-m-d');
            return substr((string)$r, 0, 10);
        } catch (\Throwable) { return date('Y-m-d'); }
    }

    // ── Catálogo ─────────────────────────────────────────────────────────────

    public function catalogoKpis(): array
    {
        $emp = $this->empresa();
        return Cache::lembrar("produtos_catalogo_kpis_{$emp}", 1800, function () use ($emp) {
            $sql = "SELECT
                COUNT(DISTINCT i.COD_ITEM)                              TOTAL_PRODUTOS,
                COUNT(DISTINCT i.COD_TIPO_PROD)                         TOTAL_CATEGORIAS,
                COUNT(DISTINCT i.COD_MARCA)                             TOTAL_MARCAS,
                COALESCE(SUM(CAST(ie.QTD AS DOUBLE PRECISION) * ie.CUSTO_FINAL), 0) VALOR_ESTOQUE,
                COUNT(CASE WHEN i.ESTOQ_MINIMO > 0 AND ie.QTD < i.ESTOQ_MINIMO THEN 1 END) ESTOQUE_BAIXO
            FROM ITENS i
            LEFT JOIN ITENS_ESTOQUE ie ON ie.COD_ITEM = i.COD_ITEM AND ie.COD_EMPRESA = {$emp}";
            return $this->pdo->query($sql)->fetch(\PDO::FETCH_ASSOC) ?: [];
        });
    }

    public function marcas(): array
    {
        return Cache::lembrar('produtos_marcas_lista', 3600, function () {
            $sql = "SELECT TRIM(m.DESCRICAO) NOME
                    FROM MARCA m
                    WHERE m.DESCRICAO IS NOT NULL AND TRIM(m.DESCRICAO) <> ''
                    ORDER BY m.DESCRICAO";
            $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
            foreach ($rows as &$r) { if (isset($r['NOME'])) $r['NOME'] = mb_convert_encoding((string)$r['NOME'], 'UTF-8', 'Windows-1252'); }
            unset($r);
            return $rows;
        });
    }

    // ── SAÚDE DO CADASTRO + PRODUTOS COM PREJUÍZO ───────────────────────────────
    // Foco em produtos COM ESTOQUE (ie.QTD > 0) — os que importam agora.

    public function saudeKpis(): array
    {
        $emp = $this->empresa();
        return Cache::lembrar("produtos_saude_kpis_{$emp}", 1800, function () use ($emp) {
            $r = $this->pdo->query("SELECT
                    COUNT(*) TOTAL,
                    SUM(CASE WHEN ie.CUSTO_FINAL IS NULL OR ie.CUSTO_FINAL <= 0 THEN 1 ELSE 0 END) SEM_CUSTO,
                    SUM(CASE WHEN ie.PRECO_VENDA IS NULL OR ie.PRECO_VENDA <= 0 THEN 1 ELSE 0 END) SEM_PRECO,
                    SUM(CASE WHEN ie.PRECO_VENDA > 0 AND ie.CUSTO_FINAL > 0 AND ie.PRECO_VENDA < ie.CUSTO_FINAL THEN 1 ELSE 0 END) PREJUIZO,
                    SUM(CASE WHEN i.NCM IS NULL OR TRIM(i.NCM) = '' THEN 1 ELSE 0 END) SEM_NCM,
                    SUM(CASE WHEN i.FOTO IS NULL THEN 1 ELSE 0 END) SEM_FOTO
                FROM ITENS_ESTOQUE ie JOIN ITENS i ON i.COD_ITEM = ie.COD_ITEM
                WHERE ie.COD_EMPRESA = {$emp} AND ie.QTD > 0")->fetch(\PDO::FETCH_ASSOC) ?: [];
            try {
                $r['SEM_BARRAS'] = (int) $this->pdo->query("SELECT COUNT(*) FROM ITENS_ESTOQUE ie
                    WHERE ie.COD_EMPRESA = {$emp} AND ie.QTD > 0 AND NOT EXISTS (SELECT 1 FROM BARRAS b WHERE b.COD_ITEM = ie.COD_ITEM)")->fetchColumn();
            } catch (\Throwable) { $r['SEM_BARRAS'] = 0; }
            return $r;
        });
    }

    /** Lista de produtos com o problema $tipo (sem_custo, sem_preco, prejuizo, sem_ncm, sem_foto, sem_barras). */
    public function saudeLista(string $tipo, int $limite = 500): array
    {
        $cond = [
            'sem_custo'  => '(ie.CUSTO_FINAL IS NULL OR ie.CUSTO_FINAL <= 0)',
            'sem_preco'  => '(ie.PRECO_VENDA IS NULL OR ie.PRECO_VENDA <= 0)',
            'prejuizo'   => 'ie.PRECO_VENDA > 0 AND ie.CUSTO_FINAL > 0 AND ie.PRECO_VENDA < ie.CUSTO_FINAL',
            'sem_ncm'    => "(i.NCM IS NULL OR TRIM(i.NCM) = '')",
            'sem_foto'   => 'i.FOTO IS NULL',
            'sem_barras' => 'NOT EXISTS (SELECT 1 FROM BARRAS b WHERE b.COD_ITEM = ie.COD_ITEM)',
        ][$tipo] ?? '1=1';
        $ord = $tipo === 'prejuizo' ? '(ie.CUSTO_FINAL - ie.PRECO_VENDA) * ie.QTD DESC' : 'ie.QTD DESC';
        $emp = $this->empresa();

        $sql = "SELECT FIRST {$limite}
                ie.COD_ITEM, TRIM(i.DESCRICAO) DESCRICAO, i.COD_UNIDADE,
                COALESCE(ie.QTD,0) QTD, COALESCE(ie.CUSTO_FINAL,0) CUSTO_FINAL,
                COALESCE(ie.PRECO_VENDA,0) PRECO_VENDA, TRIM(COALESCE(i.NCM,'')) NCM
            FROM ITENS_ESTOQUE ie JOIN ITENS i ON i.COD_ITEM = ie.COD_ITEM
            WHERE ie.COD_EMPRESA = {$emp} AND ie.QTD > 0 AND ({$cond})
            ORDER BY {$ord}";
        $rows = $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
        foreach ($rows as &$r) { $r['DESCRICAO'] = mb_convert_encoding((string)$r['DESCRICAO'], 'UTF-8', 'Windows-1252'); }
        unset($r);
        return $rows;
    }

    public function catalogo(int $pagina = 1, int $limite = 24, string $busca = '', string $marca = '', bool $apenasComFoto = true): array
    {
        $offset = ($pagina - 1) * $limite;
        $wheres = [];
        $params = [];

        if ($busca !== '') {
            $wheres[] = '(UPPER(i.DESCRICAO) CONTAINING UPPER(?) OR UPPER(TRIM(i.COD_ITEM)) CONTAINING UPPER(?))';
            $params[] = $busca;
            $params[] = $busca;
        }
        if ($marca !== '') {
            $wheres[] = 'UPPER(TRIM(m.DESCRICAO)) = UPPER(?)';
            $params[] = $marca;
        }
        if ($apenasComFoto) {
            $wheres[] = 'i.FOTO IS NOT NULL';
        }

        $whereClause = $wheres ? ('WHERE ' . implode(' AND ', $wheres)) : '';

        // Total
        $sqlCount = "SELECT COUNT(*) FROM ITENS i
                     LEFT JOIN MARCA m ON m.COD_MARCA = i.COD_MARCA
                     {$whereClause}";
        $st = $this->pdo->prepare($sqlCount);
        $st->execute($params);
        $total = (int)$st->fetchColumn();

        // Página
        $sqlData = "SELECT FIRST {$limite} SKIP {$offset}
                i.COD_ITEM,
                TRIM(i.DESCRICAO)   DESCRICAO,
                i.PRECO_VENDA,
                TRIM(m.DESCRICAO)   MARCA,
                CASE WHEN i.FOTO IS NOT NULL THEN 1 ELSE 0 END TEM_FOTO
            FROM ITENS i
            LEFT JOIN MARCA m ON m.COD_MARCA = i.COD_MARCA
            {$whereClause}
            ORDER BY i.DESCRICAO";
        $st2 = $this->pdo->prepare($sqlData);
        $st2->execute($params);
        $produtos = $st2->fetchAll(\PDO::FETCH_ASSOC);

        // Converte texto WIN1252 -> UTF-8 (senao acento gera UTF-8 invalido -> json_encode falha -> JSON vazio).
        foreach ($produtos as &$p) {
            if (isset($p['DESCRICAO'])) $p['DESCRICAO'] = mb_convert_encoding((string)$p['DESCRICAO'], 'UTF-8', 'Windows-1252');
            if (isset($p['MARCA']))     $p['MARCA']     = mb_convert_encoding((string)$p['MARCA'], 'UTF-8', 'Windows-1252');
        }
        unset($p);

        return [
            'produtos' => $produtos,
            'total'    => $total,
            'pagina'   => $pagina,
            'limite'   => $limite,
            'paginas'  => max(1, (int)ceil($total / $limite)),
        ];
    }

    public function fotoProduto(string $codItem): ?string
    {
        $st = $this->pdo->prepare("SELECT FOTO FROM ITENS WHERE COD_ITEM = ?");
        $st->execute([$codItem]);
        $row = $st->fetch(\PDO::FETCH_ASSOC);
        if (!$row || $row['FOTO'] === null) return null;
        return is_resource($row['FOTO']) ? stream_get_contents($row['FOTO']) : $row['FOTO'];
    }

    // ── Detalhe do produto ────────────────────────────────────────────────────

    public function produtoDetalhe(string $codItem): array
    {
        // Dados principais
        $sqlItem = "SELECT
            i.COD_ITEM,
            TRIM(i.DESCRICAO)           DESCRICAO,
            TRIM(i.APELIDO)             APELIDO,
            TRIM(i.COD_UNIDADE)         COD_UNIDADE,
            TRIM(i.REFERENCIA)          REFERENCIA,
            TRIM(i.NCM)                 NCM,
            TRIM(m.DESCRICAO)           MARCA,
            TRIM(tp.DESCRICAO)          TIPO_PRODUTO,
            i.PESO,
            i.ESTOQ_MINIMO,
            i.ESTOQUE_MAXIMO,
            i.QTD_EMBALAGEM,
            CASE WHEN i.FOTO IS NOT NULL THEN 1 ELSE 0 END TEM_FOTO
        FROM ITENS i
        LEFT JOIN MARCA m ON m.COD_MARCA = i.COD_MARCA
        LEFT JOIN TIPO_PRODUTO tp ON tp.COD_TIPO_PROD = i.COD_TIPO_PROD
        WHERE i.COD_ITEM = ?";
        $st = $this->pdo->prepare($sqlItem);
        $st->execute([$codItem]);
        $item = $st->fetch(\PDO::FETCH_ASSOC);
        if (!$item) return [];

        // Códigos de barras do produto
        try {
            $stB = $this->pdo->prepare("SELECT CAST(b.COD_BARRAS AS VARCHAR(30)) COD_BARRAS FROM BARRAS b WHERE b.COD_ITEM = ? ORDER BY b.COD_BARRAS");
            $stB->execute([$codItem]);
            $item['BARCODES'] = array_column($stB->fetchAll(\PDO::FETCH_ASSOC), 'COD_BARRAS');
        } catch (\Throwable $e) {
            $item['BARCODES'] = [];
        }

        // Estoque por empresa
        $sqlEst = "SELECT
            ie.COD_EMPRESA,
            TRIM(e.NOME_FANTASIA)       EMPRESA,
            ie.QTD,
            COALESCE(ie.QTD_RESERVADA,0) QTD_RESERVADA,
            ie.PRECO_VENDA,
            ie.PRECO_ATACADO,
            ie.CUSTO_FINAL,
            ie.CUSTO_MEDIO,
            ie.CUSTO_COMPRA,
            ie.PRECO_ANT1, ie.DT_ANT1,
            ie.PRECO_ANT2, ie.DT_ANT2,
            ie.PRECO_ANT3, ie.DT_ANT3,
            ie.DT_ATUALIZACAO
        FROM ITENS_ESTOQUE ie
        LEFT JOIN EMPRESAS e ON e.COD_EMPRESA = ie.COD_EMPRESA
        WHERE ie.COD_ITEM = ? AND ie.COD_EMPRESA = {$this->empresa()}
        ORDER BY ie.COD_EMPRESA";
        $st2 = $this->pdo->prepare($sqlEst);
        $st2->execute([$codItem]);
        $estoques = $st2->fetchAll(\PDO::FETCH_ASSOC);

        // Últimas vendas (histórico rápido)
        $sqlVend = "SELECT FIRST 6
            ni.DATA_EMISSAO,
            ni.QTD,
            ni.PRECO_UNIT,
            TRIM(p.NOME) CLIENTE
        FROM NOTAS_ITENS ni
        LEFT JOIN NOTAS_CAB nc ON nc.NR_NOTA = ni.NR_NOTA AND nc.COD_TIPO_OPERACAO = ni.COD_TIPO_OPERACAO
        LEFT JOIN PESSOAS p ON p.COD_PESSOA = nc.COD_PESSOA
        WHERE ni.COD_ITEM = ?
          AND ni.COD_TIPO_OPERACAO IN (115,231,297)
        ORDER BY ni.DATA_EMISSAO DESC";
        $st3 = $this->pdo->prepare($sqlVend);
        $st3->execute([$codItem]);
        $vendas = $st3->fetchAll(\PDO::FETCH_ASSOC);

        return [
            'item'     => $item,
            'estoques' => $estoques,
            'vendas'   => $vendas,
        ];
    }

    public function atualizarPreco(string $codItem, int $codEmpresa, float $novoPreco): bool
    {
        // Shift histórico e atualiza preço
        $sql = "UPDATE ITENS_ESTOQUE SET
            PRECO_ANT3      = PRECO_ANT2,
            DT_ANT3         = DT_ANT2,
            PRECO_ANT2      = PRECO_ANT1,
            DT_ANT2         = DT_ANT1,
            PRECO_ANT1      = PRECO_VENDA,
            DT_ANT1         = CURRENT_DATE,
            PRECO_VENDA     = ?,
            DT_ATUALIZACAO  = CURRENT_DATE
        WHERE COD_ITEM = ? AND COD_EMPRESA = ?";
        $st = $this->pdo->prepare($sql);
        $st->execute([$novoPreco, $codItem, $codEmpresa]);
        return $st->rowCount() > 0;
    }

    // ── Código de barras ──────────────────────────────────────────────────────

    public function buscarPorCodBarras(string $codBarras): array
    {
        $codBarras = trim($codBarras);
        $len       = strlen($codBarras);

        // O campo BARRAS.COD_BARRAS é VARCHAR(20) → cabe o EAN completo (13 díg.).
        // SEMPRE tentar o código COMPLETO primeiro (match exato) — senão caía no
        // fallback fuzzy (CONTAINING) e retornava produto errado de mesma família GS1.
        // As versões truncadas ficam só como fallback p/ cadastros antigos truncados.
        $variacoes = array_values(array_unique(array_filter([
            $codBarras,                                          // código COMPLETO (exato) — prioridade
            $len > 11  ? substr($codBarras, -11)       : null,   // ultimos 11
            $len > 11  ? substr($codBarras, 0, 11)     : null,   // primeiros 11
            $len > 12  ? substr($codBarras, 1, 11)     : null,   // da pos 1 ate 11
            $len > 13  ? substr($codBarras, 2, 11)     : null,   // da pos 2 ate 11
            $len > 8   ? substr($codBarras, -8)        : null,   // ultimos 8 (EAN-8)
            $len > 1   ? substr($codBarras, 0, min(11, $len - 1)) : null, // sem check digit
            ltrim($len > 11 ? substr($codBarras, -11) : $codBarras, '0') ?: null,
        ])));

        // Helper: executa sem CAST (campo numerico nativo) — so para valores que cabem
        $execDirect = function(string $sql, string $param): array {
            try {
                $st = $this->pdo->prepare($sql);
                $st->bindValue(1, $param, \PDO::PARAM_STR);
                $st->execute();
                return $st->fetch(\PDO::FETCH_ASSOC) ?: [];
            } catch (\Throwable) {
                return [];
            }
        };

        // 1. Busca direta na tabela BARRAS (sem CAST — evita overflow do Firebird PDO)
        foreach ($variacoes as $v) {
            $r = $execDirect(
                "SELECT FIRST 1 b.COD_ITEM, TRIM(i.DESCRICAO) DESCRICAO,
                    TRIM(m.DESCRICAO) MARCA,
                    CASE WHEN i.FOTO IS NOT NULL THEN 1 ELSE 0 END TEM_FOTO
                 FROM BARRAS b
                 JOIN ITENS i ON i.COD_ITEM = b.COD_ITEM
                 LEFT JOIN MARCA m ON m.COD_MARCA = i.COD_MARCA
                 WHERE b.COD_BARRAS = ?",
                $v
            );
            if ($r) return $r;
        }

        // 2. Busca em ITENS pelo COD_ITEM
        foreach ($variacoes as $v) {
            $r = $execDirect(
                "SELECT FIRST 1 i.COD_ITEM, TRIM(i.DESCRICAO) DESCRICAO,
                    TRIM(m.DESCRICAO) MARCA,
                    CASE WHEN i.FOTO IS NOT NULL THEN 1 ELSE 0 END TEM_FOTO
                 FROM ITENS i
                 LEFT JOIN MARCA m ON m.COD_MARCA = i.COD_MARCA
                 WHERE i.COD_ITEM = ?",
                $v
            );
            if ($r) return $r;
        }

        // 3. CONTAINING com nucleo de 7 digitos centrais (codigos chineses sem padrao).
        //    SEGURANÇA: só resolve se apontar p/ UM ÚNICO produto. Se o núcleo casar com
        //    2+ itens (família GS1 sequencial), NÃO chuta — devolve vazio (melhor "não
        //    encontrado" que produto errado). Foi o que causava o bug do COPO×GARRAFA.
        if ($len >= 7) {
            $nucleo = substr($codBarras, max(0, (int)(($len - 7) / 2)), 7);
            try {
                $st = $this->pdo->prepare(
                    "SELECT FIRST 3 DISTINCT b.COD_ITEM
                     FROM BARRAS b
                     WHERE CAST(b.COD_BARRAS AS VARCHAR(30)) CONTAINING ?"
                );
                $st->bindValue(1, $nucleo, \PDO::PARAM_STR);
                $st->execute();
                $cods = array_column($st->fetchAll(\PDO::FETCH_ASSOC), 'COD_ITEM');
                if (count($cods) === 1) {
                    $r = $execDirect(
                        "SELECT FIRST 1 i.COD_ITEM, TRIM(i.DESCRICAO) DESCRICAO,
                            TRIM(m.DESCRICAO) MARCA,
                            CASE WHEN i.FOTO IS NOT NULL THEN 1 ELSE 0 END TEM_FOTO
                         FROM ITENS i
                         LEFT JOIN MARCA m ON m.COD_MARCA = i.COD_MARCA
                         WHERE i.COD_ITEM = ?",
                        (string)$cods[0]
                    );
                    if ($r) return $r;
                }
            } catch (\Throwable) { /* ignora */ }
        }

        return [];
    }

    // ── Foto ──────────────────────────────────────────────────────────────────

    public function atualizarFoto(string $codItem, string $blob): bool
    {
        $st = $this->pdo->prepare("UPDATE ITENS SET FOTO = ? WHERE COD_ITEM = ?");
        $st->bindParam(1, $blob, \PDO::PARAM_LOB);
        $st->bindParam(2, $codItem);
        $st->execute();
        return $st->rowCount() > 0;
    }

    // ── Ajuste de estoque ─────────────────────────────────────────────────────

    public function ajustarEstoque(string $codItem, int $codEmpresa, float $qtd, string $tipo): array
    {
        // tipo: 'ajuste' (valor absoluto), 'entrada' (+), 'saida' (-)
        if ($tipo === 'ajuste') {
            $sql = "UPDATE ITENS_ESTOQUE SET QTD = ? WHERE COD_ITEM = ? AND COD_EMPRESA = ?";
            $st  = $this->pdo->prepare($sql);
            $st->execute([$qtd, $codItem, $codEmpresa]);
        } else {
            $delta = $tipo === 'saida' ? -abs($qtd) : abs($qtd);
            $sql   = "UPDATE ITENS_ESTOQUE SET QTD = QTD + ? WHERE COD_ITEM = ? AND COD_EMPRESA = ?";
            $st    = $this->pdo->prepare($sql);
            $st->execute([$delta, $codItem, $codEmpresa]);
        }
        if ($st->rowCount() === 0) {
            return ['ok' => false, 'erro' => 'Registro não encontrado'];
        }
        // Retorna qtd atualizada
        $st2 = $this->pdo->prepare("SELECT QTD FROM ITENS_ESTOQUE WHERE COD_ITEM = ? AND COD_EMPRESA = ?");
        $st2->execute([$codItem, $codEmpresa]);
        $row = $st2->fetch(\PDO::FETCH_ASSOC);
        return ['ok' => true, 'qtd_nova' => $row ? (float)$row['QTD'] : null];
    }

    // ── Atualizar campos do produto ───────────────────────────────────────────

    public function atualizarCamposProduto(string $codItem, array $campos): bool
    {
        $permitidos = ['DESCRICAO', 'APELIDO', 'NCM', 'ESTOQ_MINIMO', 'ESTOQUE_MAXIMO', 'PESO', 'QTD_EMBALAGEM', 'COD_UNIDADE'];
        $sets = []; $params = [];
        foreach ($campos as $col => $val) {
            $col = strtoupper($col);
            if (in_array($col, $permitidos, true)) {
                $sets[]   = "$col = ?";
                $params[] = ($val === '' || $val === null) ? null : $val;
            }
        }
        if (empty($sets)) return false;
        $params[] = $codItem;
        $st = $this->pdo->prepare("UPDATE ITENS SET " . implode(', ', $sets) . " WHERE COD_ITEM = ?");
        $st->execute($params);
        return $st->rowCount() > 0;
    }

    // ── Atualizar preço atacado ───────────────────────────────────────────────

    public function atualizarPrecoAtacado(string $codItem, int $codEmpresa, float $novoPreco): bool
    {
        $st = $this->pdo->prepare("UPDATE ITENS_ESTOQUE SET PRECO_ATACADO = ?, DT_ATUALIZACAO = CURRENT_DATE WHERE COD_ITEM = ? AND COD_EMPRESA = ?");
        $st->execute([$novoPreco, $codItem, $codEmpresa]);
        return $st->rowCount() > 0;
    }

    // ════════════════════════════════════════════════════════════════════════
    // Depto de Engenharia e Automação - Master Automação - Autor: Derlivon Silva
    //
    // cadastroFiscal() — dump do cadastro fiscal por produto pra o PDF Fiscal.
    //
    // Premissa (confirmada via schema do Real Prime, 2026-06-16):
    //   - ITENS centraliza TODAS as colunas fiscais (NCM, CEST, CST, ICMS, PIS,
    //     COFINS, IPI, CFOP, ANP, NFSE_*, CBENEF, CCLASSTRIB, etc).
    //   - ITENS_ESTOQUE so guarda preco/custo/estoque por empresa (sem fiscal).
    //   - Aliquotas sao por PRODUTO (nao por empresa).
    //
    // Graceful degrade: colunas que so existem em Fire >= 2025 (CCLASSTRIB,
    // CBENEF, NFSE_*) podem nao existir em bancos antigos. Se o SELECT
    // completo falhar (SQLCODE -206 column unknown), tenta um SELECT minimo
    // com SO as colunas garantidas (presentes desde os primeiros Fire). As
    // colunas ausentes voltam como '' / 0 / null pra view exibir "—".
    // ════════════════════════════════════════════════════════════════════════
    public function cadastroFiscal(int $limite = 999999, string $busca = ''): array
    {
        $emp    = $this->empresa();
        $limite = max(1, min($limite, 999999));
        $busca  = trim($busca);

        // Cache curto (30 min) — chave inclui busca pra nao misturar resultados.
        $chaveBusca = $busca === '' ? 'all' : substr(md5($busca), 0, 10);
        return Cache::lembrar("produtos_cadastro_fiscal_{$emp}_{$limite}_{$chaveBusca}", 1800, function () use ($emp, $limite, $busca) {

            // Filtro de busca (numerico -> COD_ITEM, texto -> DESCRICAO LIKE).
            $condBusca = '';
            $params    = [];
            if ($busca !== '') {
                if (ctype_digit($busca)) {
                    $condBusca = ' AND i.COD_ITEM = :busca_cod';
                    $params[':busca_cod'] = (int)$busca;
                } else {
                    $condBusca = ' AND UPPER(i.DESCRICAO) LIKE UPPER(:busca_txt)';
                    $params[':busca_txt'] = '%' . $busca . '%';
                }
            }

            // ── SELECT completo (Fire >= 2025, com Reforma IBS/CBS) ──────────
            $sqlCompleto = "SELECT FIRST {$limite}
                    i.COD_ITEM,
                    TRIM(i.DESCRICAO)                              DESCRICAO,
                    TRIM(COALESCE(tp.DESCRICAO,''))                TIPO_PRODUTO,
                    TRIM(COALESCE(i.NCM,''))                       NCM,
                    TRIM(COALESCE(i.CEST,''))                      CEST,
                    i.COD_ORIGEM                                   ORIGEM_MERCADORIA,
                    TRIM(COALESCE(i.CBENEF,''))                    CBENEF,
                    TRIM(COALESCE(i.CCLASSTRIB,''))                CCLASSTRIB,
                    TRIM(COALESCE(i.FIS_COD_SITTRIB,''))           CST_PF_DENTRO,
                    TRIM(COALESCE(i.JUR_COD_SITTRIB,''))           CST_PJ_DENTRO,
                    TRIM(COALESCE(i.FORA_FIS_COD_SITTRIB,''))      CST_PF_FORA,
                    TRIM(COALESCE(i.FORA_JUR_COD_SITTRIB,''))      CST_PJ_FORA,
                    COALESCE(i.PERC_DENTRO_ESTADO, 0)              ALIQ_ICMS,
                    COALESCE(i.PERC_FORA_ESTADO,   0)              ALIQ_ICMS_INTER,
                    COALESCE(i.MARGEM_LUCRO_SUBST, 0)              MVA_ICMS_ST,
                    TRIM(COALESCE(i.COD_CST_PIS,''))               CST_PIS_SAIDA,
                    TRIM(COALESCE(i.COD_CST_COFINS,''))            CST_COFINS_SAIDA,
                    COALESCE(i.CFG_PIS,    0)                      ALIQ_PIS,
                    COALESCE(i.CFG_COFINS, 0)                      ALIQ_COFINS,
                    TRIM(COALESCE(i.CST_IPI_SAIDA,''))             CST_IPI,
                    COALESCE(i.PERC_IPI, 0)                        ALIQ_IPI,
                    TRIM(COALESCE(i.CFOP_COMPRA_EST,''))           CFOP_COMPRA_INT,
                    TRIM(COALESCE(i.CFOP_COMPRA_INT,''))           CFOP_COMPRA_FORA,
                    COALESCE(ie.PRECO_VENDA, 0)                    PRECO_VENDA,
                    COALESCE(ie.CUSTO_FINAL, 0)                    CUSTO_FINAL
                FROM ITENS i
                LEFT JOIN TIPO_PRODUTO  tp ON tp.COD_TIPO_PROD = i.COD_TIPO_PROD
                LEFT JOIN ITENS_ESTOQUE ie ON ie.COD_ITEM = i.COD_ITEM AND ie.COD_EMPRESA = {$emp}
                WHERE 1=1
                  {$condBusca}
                ORDER BY TRIM(i.DESCRICAO)";

            // ── SELECT minimo (Fire antigo, sem CBENEF/CCLASSTRIB) ───────────
            $sqlMinimo = "SELECT FIRST {$limite}
                    i.COD_ITEM,
                    TRIM(i.DESCRICAO)                              DESCRICAO,
                    TRIM(COALESCE(tp.DESCRICAO,''))                TIPO_PRODUTO,
                    TRIM(COALESCE(i.NCM,''))                       NCM,
                    TRIM(COALESCE(i.CEST,''))                      CEST,
                    i.COD_ORIGEM                                   ORIGEM_MERCADORIA,
                    CAST('' AS VARCHAR(15))                        CBENEF,
                    CAST('' AS VARCHAR(10))                        CCLASSTRIB,
                    TRIM(COALESCE(i.FIS_COD_SITTRIB,''))           CST_PF_DENTRO,
                    TRIM(COALESCE(i.JUR_COD_SITTRIB,''))           CST_PJ_DENTRO,
                    TRIM(COALESCE(i.FORA_FIS_COD_SITTRIB,''))      CST_PF_FORA,
                    TRIM(COALESCE(i.FORA_JUR_COD_SITTRIB,''))      CST_PJ_FORA,
                    COALESCE(i.PERC_DENTRO_ESTADO, 0)              ALIQ_ICMS,
                    COALESCE(i.PERC_FORA_ESTADO,   0)              ALIQ_ICMS_INTER,
                    COALESCE(i.MARGEM_LUCRO_SUBST, 0)              MVA_ICMS_ST,
                    TRIM(COALESCE(i.COD_CST_PIS,''))               CST_PIS_SAIDA,
                    TRIM(COALESCE(i.COD_CST_COFINS,''))            CST_COFINS_SAIDA,
                    COALESCE(i.CFG_PIS,    0)                      ALIQ_PIS,
                    COALESCE(i.CFG_COFINS, 0)                      ALIQ_COFINS,
                    TRIM(COALESCE(i.CST_IPI_SAIDA,''))             CST_IPI,
                    COALESCE(i.PERC_IPI, 0)                        ALIQ_IPI,
                    TRIM(COALESCE(i.CFOP_COMPRA_EST,''))           CFOP_COMPRA_INT,
                    TRIM(COALESCE(i.CFOP_COMPRA_INT,''))           CFOP_COMPRA_FORA,
                    COALESCE(ie.PRECO_VENDA, 0)                    PRECO_VENDA,
                    COALESCE(ie.CUSTO_FINAL, 0)                    CUSTO_FINAL
                FROM ITENS i
                LEFT JOIN TIPO_PRODUTO  tp ON tp.COD_TIPO_PROD = i.COD_TIPO_PROD
                LEFT JOIN ITENS_ESTOQUE ie ON ie.COD_ITEM = i.COD_ITEM AND ie.COD_EMPRESA = {$emp}
                WHERE 1=1
                  {$condBusca}
                ORDER BY TRIM(i.DESCRICAO)";

            // ── SELECT ultra-minimo (Fire MUITO antigo, so o que ProdutosRepository ja usa) ─
            $sqlUltra = "SELECT FIRST {$limite}
                    i.COD_ITEM,
                    TRIM(i.DESCRICAO)                              DESCRICAO,
                    CAST('' AS VARCHAR(60))                        TIPO_PRODUTO,
                    TRIM(COALESCE(i.NCM,''))                       NCM,
                    CAST('' AS VARCHAR(7))                         CEST,
                    CAST(NULL AS INTEGER)                          ORIGEM_MERCADORIA,
                    CAST('' AS VARCHAR(15))                        CBENEF,
                    CAST('' AS VARCHAR(10))                        CCLASSTRIB,
                    CAST('' AS VARCHAR(3))                         CST_PF_DENTRO,
                    CAST('' AS VARCHAR(3))                         CST_PJ_DENTRO,
                    CAST('' AS VARCHAR(3))                         CST_PF_FORA,
                    CAST('' AS VARCHAR(3))                         CST_PJ_FORA,
                    CAST(0 AS NUMERIC(15,4))                       ALIQ_ICMS,
                    CAST(0 AS NUMERIC(15,4))                       ALIQ_ICMS_INTER,
                    CAST(0 AS NUMERIC(15,4))                       MVA_ICMS_ST,
                    CAST('' AS VARCHAR(2))                         CST_PIS_SAIDA,
                    CAST('' AS VARCHAR(2))                         CST_COFINS_SAIDA,
                    CAST(0 AS NUMERIC(15,4))                       ALIQ_PIS,
                    CAST(0 AS NUMERIC(15,4))                       ALIQ_COFINS,
                    CAST('' AS VARCHAR(2))                         CST_IPI,
                    CAST(0 AS NUMERIC(15,4))                       ALIQ_IPI,
                    CAST('' AS VARCHAR(4))                         CFOP_COMPRA_INT,
                    CAST('' AS VARCHAR(4))                         CFOP_COMPRA_FORA,
                    COALESCE(ie.PRECO_VENDA, 0)                    PRECO_VENDA,
                    COALESCE(ie.CUSTO_FINAL, 0)                    CUSTO_FINAL
                FROM ITENS i
                LEFT JOIN ITENS_ESTOQUE ie ON ie.COD_ITEM = i.COD_ITEM AND ie.COD_EMPRESA = {$emp}
                WHERE 1=1
                  {$condBusca}
                ORDER BY TRIM(i.DESCRICAO)";

            $rows = $this->executarComFallback([$sqlCompleto, $sqlMinimo, $sqlUltra], $params);

            // Converte WIN1252 -> UTF-8 nos textos e classifica status fiscal.
            foreach ($rows as &$r) {
                foreach (['DESCRICAO','TIPO_PRODUTO','NCM','CEST','CBENEF','CCLASSTRIB',
                          'CST_PF_DENTRO','CST_PJ_DENTRO','CST_PF_FORA','CST_PJ_FORA',
                          'CST_PIS_SAIDA','CST_COFINS_SAIDA','CST_IPI',
                          'CFOP_COMPRA_INT','CFOP_COMPRA_FORA'] as $col) {
                    if (isset($r[$col])) $r[$col] = $this->utf8($r[$col]);
                }

                // Pega o CST/CSOSN "principal" pra a coluna sintetica da tabela.
                // Prioridade: JUR_DENTRO (PJ saida interna) > FIS_DENTRO > qualquer outro.
                $cstPrincipal = '';
                foreach (['CST_PJ_DENTRO','CST_PF_DENTRO','CST_PJ_FORA','CST_PF_FORA'] as $col) {
                    if (!empty($r[$col]) && trim((string)$r[$col]) !== '') {
                        $cstPrincipal = trim((string)$r[$col]);
                        break;
                    }
                }
                $r['CST_PRINCIPAL'] = $cstPrincipal;

                // Status fiscal (texto, sem cor — view decide a apresentacao)
                $ncm    = trim((string)($r['NCM'] ?? ''));
                $status = 'OK';
                if ($ncm === '') {
                    $status = 'SEM NCM';
                } elseif (!ctype_digit($ncm) || strlen($ncm) !== 8) {
                    $status = 'NCM INVALIDO';
                } elseif ($cstPrincipal === '') {
                    $status = 'SEM CST';
                }
                $r['STATUS_FISCAL'] = $status;
            }
            unset($r);

            return $rows;
        });
    }

    /**
     * Helper interno: roda SQLs em ordem, caindo no proximo se o anterior
     * falhar por coluna desconhecida (SQLCODE -206). Usado por cadastroFiscal()
     * pra suportar Fire de varias eras sem precisar detectar versao.
     *
     * @param array<int,string> $sqls
     * @param array<string,mixed> $params
     * @return array<int,array<string,mixed>>
     */
    private function executarComFallback(array $sqls, array $params = []): array
    {
        $ultErro = null;
        foreach ($sqls as $sql) {
            try {
                if ($params) {
                    $st = $this->pdo->prepare($sql);
                    $st->execute($params);
                    return $st->fetchAll(\PDO::FETCH_ASSOC);
                }
                return $this->pdo->query($sql)->fetchAll(\PDO::FETCH_ASSOC);
            } catch (\Throwable $e) {
                $ultErro = $e;
                $msg = strtolower($e->getMessage());
                // Coluna desconhecida -> tenta proxima variante. Outros erros -> aborta.
                if (strpos($msg, 'column unknown') === false
                    && strpos($msg, 'undefined name') === false
                    && strpos($msg, '-206') === false) {
                    throw $e;
                }
            }
        }
        // Esgotou todas as variantes: bola pra cima pra controller logar.
        if ($ultErro) throw $ultErro;
        return [];
    }
}
