<?php

namespace App\Repositories;

use App\Core\Cache;

/**
 * Sugestão de Compra — o que repor e quanto.
 *
 * Cruza 4 lógicas (escolhidas pelo gestor):
 *   1. Giro + cobertura-alvo   → repor para cobrir N dias de venda.
 *   2. Estoque mínimo do ERP    → repor quando QTD < ESTOQ_MINIMO.
 *   3. Ruptura                  → estoque <= 0 que ainda vende (venda perdida).
 *   4. Lead time do fornecedor  → soma o tempo médio de reposição à cobertura-alvo.
 *
 * Arquitetura de performance (lições do projeto):
 *   - NÚCLEO rápido (~6s): dirige pela AGREGAÇÃO de vendas (poucos itens) + lista de mínimos
 *     limitada; nunca varre o estoque inteiro na memória. Cacheado.
 *   - FORNECEDOR/LEAD são caros (varrem NOTAS_ITENS de compra) → opcionais, em mapas
 *     cacheados 24h, carregados em 2º plano (igual "custos off por padrão" nos relatórios).
 *   - Filtra SEMPRE por COD_EMPRESA (ITENS_ESTOQUE/NOTAS têm 3 empresas no mesmo banco).
 *   - Texto WIN1252 → UTF-8 antes de devolver (senão json_encode esvazia).
 */
class SugestaoCompraRepository
{
    /** Operações de VENDA (mesmo conjunto usado no Giro de Estoque, validado). */
    private const OPS_VENDA = '115,231,297';

    public function __construct(private \PDO $pdo, private int $codEmpresa = 5) {}

    /**
     * Contagem rápida de itens em RUPTURA (estoque <= 0 com vendas) OU
     * abaixo do mínimo. Query enxuta — não roda a sugestao() completa
     * (que processa enriquecimento e cálculos pesados).
     *
     * Cacheado 5 min — pra ser chamado em CADA requisição na sidebar
     * sem onerar o Firebird.
     *
     * Depto de Engenharia e Automação - Master Automação - Autor: Derlivon Silva
     */
    public function rupturaCount(): int
    {
        $emp = $this->codEmpresa;
        $ck  = 'sugestao_ruptura_count_' . $emp;
        return (int) Cache::lembrar($ck, 300, function () use ($emp) {
            try {
                // Itens abaixo do mínimo OU em ruptura efetiva (QTD <= 0 e já vendeu nos últimos 60d)
                $sql = "SELECT COUNT(*) FROM (
                            SELECT ie.COD_ITEM
                            FROM ITENS_ESTOQUE ie
                            JOIN ITENS i ON i.COD_ITEM = ie.COD_ITEM
                            WHERE ie.COD_EMPRESA = {$emp}
                              AND i.ESTOQ_MINIMO > 0
                              AND ie.QTD < i.ESTOQ_MINIMO
                        ) X";
                $r = $this->pdo->query($sql)->fetchColumn();
                return (int)$r;
            } catch (\Throwable) { return 0; }
        });
    }

    /** Última data de venda (fallback p/ período padrão). */
    public function ultimaData(): string
    {
        return Cache::lembrar('sugestao_ultima_data', 600, function () {
            try {
                // Janela recente (120d) — MAX sem limite custa ~45s no Firebird 2.5 sem índice descendente.
                $r = $this->pdo->query(
                    "SELECT MAX(nc.DATA_EMISSAO)
                     FROM NOTAS_CAB nc
                     WHERE nc.COD_TIPO_OPERACAO IN (" . self::OPS_VENDA . ")
                       AND nc.COD_SITUACAO <> 3
                       AND nc.COD_EMPRESA = {$this->codEmpresa}
                       AND nc.DATA_EMISSAO >= DATEADD(DAY,-120,CURRENT_DATE)
                       AND nc.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'); }
        });
    }

    /**
     * Ponto de entrada. $f:
     *   data_ini, data_fim  : período de venda p/ velocidade de giro
     *   cobertura           : dias de cobertura-alvo (default 30)
     *   lead_padrao         : lead time padrão em dias (default 7)
     *   apenas              : '' | 'ruptura' | 'minimo' | 'giro'
     *   incluir_sem_venda   : bool — incluir abaixo-do-mínimo que não vendeu (default true)
     *   ordenar             : 'urgencia' | 'valor' | 'fornecedor'
     *   limite              : nº máx. de linhas (default 1000)
     *   com_fornecedor      : bool — anexar fornecedor + lead time (mais lento)
     *   usar_lead           : bool — somar lead time do fornecedor à cobertura
     *   forn_ini, forn_fim  : faixa de código de fornecedor (só com com_fornecedor)
     */
    public function sugestao(array $f): array
    {
        $core = $this->candidatos($f);
        if (!empty($f['com_fornecedor'])) {
            $core = $this->enriquecer($core, $f);
        }
        return $core;
    }

    // ── NÚCLEO (rápido, cacheado) ─────────────────────────────────────────────
    private function candidatos(array $f): array
    {
        $dataIni  = $f['data_ini'] ?? date('Y-m-d', strtotime('-29 days'));
        $dataFim  = $f['data_fim'] ?? date('Y-m-d');
        $cobertura = max(1, (int)($f['cobertura'] ?? 30));
        $leadPad   = max(0, (int)($f['lead_padrao'] ?? 7));
        $apenas    = (string)($f['apenas'] ?? '');
        $incSemVenda = !isset($f['incluir_sem_venda']) || !empty($f['incluir_sem_venda']);
        $ordenar   = (string)($f['ordenar'] ?? 'urgencia');
        $limite    = min(5000, max(10, (int)($f['limite'] ?? 1000)));
        $emp       = $this->codEmpresa;

        $chave = 'sugestao_core_' . md5(json_encode([
            $emp, $dataIni, $dataFim, $cobertura, $leadPad, $apenas, $incSemVenda, $ordenar, $limite,
        ]));
        $ttl = ($dataFim >= date('Y-m-d')) ? 600 : 3600;

        return Cache::lembrar($chave, $ttl, function () use (
            $dataIni, $dataFim, $cobertura, $leadPad, $apenas, $incSemVenda, $ordenar, $limite, $emp
        ) {
            $dias = max(1, (int)round((strtotime($dataFim) - strtotime($dataIni)) / 86400) + 1);
            $cand = [];

            // 1) Candidatos por VENDA (driver = agregação de vendas)
            $sqlV = "SELECT
                    v.COD_ITEM, i.DESCRICAO, i.COD_UNIDADE,
                    i.ESTOQ_MINIMO, i.ESTOQUE_MAXIMO, i.QTD_EMBALAGEM,
                    ie.QTD ESTOQUE_ATUAL, ie.CUSTO_FINAL, ie.CUSTO_COMPRA, ie.CUSTO_MEDIO, ie.PRECO_VENDA,
                    v.QTD_VENDIDA, v.FAT_VENDIDA
                FROM (
                    SELECT ni.COD_ITEM, SUM(ni.QTD) QTD_VENDIDA, SUM(ni.QTD * ni.PRECO_UNIT) FAT_VENDIDA
                    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 nc.COD_TIPO_OPERACAO IN (" . self::OPS_VENDA . ")
                      AND nc.COD_SITUACAO <> 3
                      AND nc.COD_EMPRESA = {$emp}
                      AND nc.DATA_EMISSAO BETWEEN ? AND ?
                    GROUP BY ni.COD_ITEM
                ) v
                JOIN ITENS_ESTOQUE ie ON ie.COD_ITEM = v.COD_ITEM AND ie.COD_EMPRESA = {$emp}
                JOIN ITENS i          ON i.COD_ITEM  = v.COD_ITEM";
            $st = $this->pdo->prepare($sqlV);
            $st->execute([$dataIni, $dataFim]);
            foreach ($st->fetchAll(\PDO::FETCH_ASSOC) as $r) {
                $cand[(int)$r['COD_ITEM']] = $this->base($r, (float)$r['QTD_VENDIDA'], (float)$r['FAT_VENDIDA'], $dias);
            }

            // 2) Abaixo do mínimo (mesmo sem venda) — FIRST + ORDER BY déficit (não varre tudo)
            if ($incSemVenda) {
                $sqlM = "SELECT FIRST {$limite}
                        ie.COD_ITEM, i.DESCRICAO, i.COD_UNIDADE,
                        i.ESTOQ_MINIMO, i.ESTOQUE_MAXIMO, i.QTD_EMBALAGEM,
                        ie.QTD ESTOQUE_ATUAL, ie.CUSTO_FINAL, ie.CUSTO_COMPRA, ie.CUSTO_MEDIO, ie.PRECO_VENDA
                    FROM ITENS_ESTOQUE ie
                    JOIN ITENS i ON i.COD_ITEM = ie.COD_ITEM
                    WHERE ie.COD_EMPRESA = {$emp}
                      AND i.ESTOQ_MINIMO > 0 AND ie.QTD < i.ESTOQ_MINIMO
                    ORDER BY (i.ESTOQ_MINIMO - ie.QTD) DESC";
                foreach ($this->pdo->query($sqlM)->fetchAll(\PDO::FETCH_ASSOC) as $r) {
                    $cod = (int)$r['COD_ITEM'];
                    if (isset($cand[$cod])) continue;
                    $cand[$cod] = $this->base($r, 0.0, 0.0, $dias);
                }
            }

            if (!$cand) return $this->vazio($dataIni, $dataFim, $cobertura, $dias);

            // 2.5) TENDÊNCIA: vendas do período ANTERIOR (mesmo tamanho) p/ ↑/↓
            $prevFim = date('Y-m-d', strtotime($dataIni . ' -1 day'));
            $prevIni = date('Y-m-d', strtotime($prevFim . ' -' . ($dias - 1) . ' days'));
            $vendaAnt = [];
            $stp = $this->pdo->prepare(
                "SELECT ni.COD_ITEM, SUM(ni.QTD) Q
                 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 nc.COD_TIPO_OPERACAO IN (" . self::OPS_VENDA . ")
                   AND nc.COD_SITUACAO <> 3 AND nc.COD_EMPRESA = {$emp}
                   AND nc.DATA_EMISSAO BETWEEN ? AND ?
                 GROUP BY ni.COD_ITEM");
            $stp->execute([$prevIni, $prevFim]);
            foreach ($stp->fetchAll(\PDO::FETCH_ASSOC) as $r) { $vendaAnt[(int)$r['COD_ITEM']] = (float)$r['Q']; }

            // 2.6) CLASSE ABC (relevância no faturamento do período, sobre os candidatos)
            $fatTotal = 0.0;
            foreach ($cand as $c) { $fatTotal += $c['FAT_VENDIDA']; }
            $ord = $cand;
            uasort($ord, fn($a, $b) => $b['FAT_VENDIDA'] <=> $a['FAT_VENDIDA']);
            $acum = 0.0;
            foreach ($ord as $cod => $c) {
                $acum += $c['FAT_VENDIDA'];
                $pa = $fatTotal > 0 ? ($acum / $fatTotal * 100) : 100;
                $cand[$cod]['CLASSE_ABC'] = ($c['FAT_VENDIDA'] <= 0) ? 'C' : ($pa <= 70 ? 'A' : ($pa <= 90 ? 'B' : 'C'));
            }

            // 2.7) aplica tendência por item
            foreach ($cand as $cod => &$c) {
                $vdaAnt = ($vendaAnt[$cod] ?? 0) > 0 ? $vendaAnt[$cod] / $dias : 0.0;
                $c['VENDA_DIA_ANT'] = $vdaAnt;
                if ($c['VENDA_DIA'] <= 0 && $vdaAnt <= 0)      $c['TENDENCIA'] = 'flat';
                elseif ($vdaAnt <= 0)                          $c['TENDENCIA'] = 'up';
                else {
                    $chg = ($c['VENDA_DIA'] - $vdaAnt) / $vdaAnt;
                    $c['TENDENCIA'] = $chg > 0.15 ? 'up' : ($chg < -0.15 ? 'down' : 'flat');
                }
            }
            unset($c);

            // 3) Sugestão (lead padrão) + motivos + urgência
            foreach ($cand as &$c) { $this->calcular($c, $cobertura, $leadPad); }
            unset($c);

            // Só quem precisa repor
            $cand = array_filter($cand, fn($c) => $c['SUGESTAO_QTD'] > 0 || $c['MOTIVO_RUPTURA'] || $c['MOTIVO_MINIMO']);
            // Filtro de motivo
            if ($apenas === 'ruptura') $cand = array_filter($cand, fn($c) => $c['MOTIVO_RUPTURA']);
            if ($apenas === 'minimo')  $cand = array_filter($cand, fn($c) => $c['MOTIVO_MINIMO']);
            if ($apenas === 'giro')    $cand = array_filter($cand, fn($c) => $c['MOTIVO_GIRO']);

            uasort($cand, fn($a, $b) => $b['URGENCIA'] <=> $a['URGENCIA']);
            $cand = array_slice($cand, 0, $limite, true);

            $linhas = array_values($cand);
            if ($ordenar === 'valor') usort($linhas, fn($a, $b) => $b['VALOR_SUGERIDO'] <=> $a['VALOR_SUGERIDO']);

            return $this->montar($linhas, $dataIni, $dataFim, $dias, $cobertura, false);
        });
    }

    // ── ENRIQUECIMENTO (fornecedor + lead, cacheado 24h, em 2º plano) ──────────
    private function enriquecer(array $core, array $f): array
    {
        $linhas = $core['linhas'];
        if (!$linhas) return $core;

        $dataFim  = $core['dataFim'];
        $cobertura = (int)$core['cobertura'];
        $leadPad   = max(0, (int)($f['lead_padrao'] ?? 7));
        $usarLead  = !empty($f['usar_lead']);
        $ordenar   = (string)($f['ordenar'] ?? 'urgencia');
        $fornIni   = isset($f['forn_ini']) && $f['forn_ini'] !== '' ? (int)$f['forn_ini'] : null;
        $fornFim   = isset($f['forn_fim']) && $f['forn_fim'] !== '' ? (int)$f['forn_fim'] : null;

        $mapaForn = $this->mapaFornecedor($dataFim);   // cod_item => {cod,nome,custo,data}
        $mapaLead = $usarLead ? $this->mapaLead($dataFim, $leadPad) : []; // cod_pessoa => dias

        $out = [];
        foreach ($linhas as $c) {
            $fn = $mapaForn[$c['COD_ITEM']] ?? null;
            if ($fn) {
                $c['FORNECEDOR_COD']  = $fn['cod'];
                $c['FORNECEDOR_NOME'] = $fn['nome'];
                $c['ULT_CUSTO']       = $fn['custo'];
                $c['ULT_COMPRA']      = $fn['data'];
            }
            // filtro por faixa de fornecedor
            if ($fornIni !== null && ($c['FORNECEDOR_COD'] === null || (int)$c['FORNECEDOR_COD'] < $fornIni)) continue;
            if ($fornFim !== null && ($c['FORNECEDOR_COD'] === null || (int)$c['FORNECEDOR_COD'] > $fornFim)) continue;

            $lead = ($usarLead && $c['FORNECEDOR_COD'] !== null)
                ? ($mapaLead[(int)$c['FORNECEDOR_COD']] ?? $leadPad)
                : ($usarLead ? $leadPad : 0);
            $this->calcular($c, $cobertura, $lead);
            $out[] = $c;
        }

        if ($ordenar === 'fornecedor') {
            usort($out, fn($a, $b) => strcmp((string)$a['FORNECEDOR_NOME'], (string)$b['FORNECEDOR_NOME'])
                                      ?: ($b['URGENCIA'] <=> $a['URGENCIA']));
        } elseif ($ordenar === 'valor') {
            usort($out, fn($a, $b) => $b['VALOR_SUGERIDO'] <=> $a['VALOR_SUGERIDO']);
        }

        return $this->montar($out, $core['dataIni'], $dataFim, (int)$core['dias'], $cobertura, true);
    }

    // ── mapas pesados, cacheados 24h ───────────────────────────────────────────

    /** cod_item => fornecedor mais recente (último que nos vendeu o item) nos últimos 180 dias. */
    private function mapaFornecedor(string $dataFim): array
    {
        $ini = date('Y-m-d', strtotime($dataFim . ' -180 days'));
        $chave = 'sugestao_forn_' . $this->codEmpresa . '_' . md5($ini . $dataFim);
        return Cache::lembrar($chave, 86400, function () use ($ini, $dataFim) {
            $emp = $this->codEmpresa;
            $sql = "SELECT ni.COD_ITEM, nc.COD_PESSOA,
                           TRIM(COALESCE(p.NOME,'')) NOME,
                           MAX(ni.DATA_EMISSAO) ULT_DATA, AVG(ni.PRECO_UNIT) PRECO_MED
                    FROM NOTAS_ITENS ni
                    JOIN TIPO_OPERACAO t ON ni.COD_TIPO_OPERACAO = t.COD_TIPO_OPERACAO AND t.TIPO_NATUREZA = 'C'
                    JOIN NOTAS_CAB nc    ON nc.NR_NOTA = ni.NR_NOTA
                    LEFT JOIN PESSOAS p  ON p.COD_PESSOA = nc.COD_PESSOA
                    WHERE ni.DATA_EMISSAO BETWEEN ? AND ?
                      AND ni.COD_EMPRESA = {$emp}
                      AND (nc.COD_SITUACAO IS NULL OR nc.COD_SITUACAO <> 3)
                      AND nc.COD_PESSOA > 1
                    GROUP BY ni.COD_ITEM, nc.COD_PESSOA, TRIM(COALESCE(p.NOME,''))";
            $st = $this->pdo->prepare($sql);
            $st->execute([$ini, $dataFim]);
            $map = [];
            foreach ($st->fetchAll(\PDO::FETCH_ASSOC) as $r) {
                $nome = $this->utf($r['NOME'] ?? '');
                if ($nome === '' || stripos($nome, 'CONSUMIDOR') !== false) continue; // não é fornecedor real
                $cod  = (int)$r['COD_ITEM'];
                $data = substr((string)$r['ULT_DATA'], 0, 10);
                if (!isset($map[$cod]) || $data > $map[$cod]['data']) {
                    $map[$cod] = [
                        'cod'   => (int)$r['COD_PESSOA'],
                        'nome'  => $nome,
                        'custo' => (float)$r['PRECO_MED'],
                        'data'  => $data,
                    ];
                }
            }
            return $map;
        });
    }

    /** cod_pessoa => lead time (dias) = intervalo médio entre compras no último ano. */
    private function mapaLead(string $dataFim, int $leadPad): array
    {
        $ini = date('Y-m-d', strtotime($dataFim . ' -365 days'));
        $chave = 'sugestao_lead_' . $this->codEmpresa . '_' . md5($ini . $dataFim);
        return Cache::lembrar($chave, 86400, function () use ($ini, $dataFim, $leadPad) {
            $sql = "SELECT nc.COD_PESSOA,
                           MIN(nc.DATA_EMISSAO) DT_MIN, MAX(nc.DATA_EMISSAO) DT_MAX,
                           COUNT(DISTINCT nc.DATA_EMISSAO) N
                    FROM NOTAS_CAB nc
                    JOIN TIPO_OPERACAO t ON nc.COD_TIPO_OPERACAO = t.COD_TIPO_OPERACAO AND t.TIPO_NATUREZA = 'C'
                    WHERE nc.DATA_EMISSAO BETWEEN ? AND ?
                      AND nc.COD_EMPRESA = {$this->codEmpresa}
                      AND (nc.COD_SITUACAO IS NULL OR nc.COD_SITUACAO <> 3)
                      AND nc.COD_PESSOA > 1
                    GROUP BY nc.COD_PESSOA";
            $st = $this->pdo->prepare($sql);
            $st->execute([$ini, $dataFim]);
            $out = [];
            foreach ($st->fetchAll(\PDO::FETCH_ASSOC) as $r) {
                $n = (int)$r['N'];
                if ($n < 2) { $out[(int)$r['COD_PESSOA']] = $leadPad; continue; }
                $span = (strtotime(substr((string)$r['DT_MAX'],0,10)) - strtotime(substr((string)$r['DT_MIN'],0,10))) / 86400;
                $out[(int)$r['COD_PESSOA']] = max(1, min(90, (int)round($span / ($n - 1))));
            }
            return $out;
        });
    }

    // ── helpers ────────────────────────────────────────────────────────────────

    private function base(array $r, float $qtdVend, float $fat, int $dias): array
    {
        $row = [
            'COD_ITEM'      => (int)$r['COD_ITEM'],
            'DESCRICAO'     => $this->utf($r['DESCRICAO'] ?? ''),
            'COD_UNIDADE'   => $this->utf($r['COD_UNIDADE'] ?? ''),
            'ESTOQUE_ATUAL' => (float)($r['ESTOQUE_ATUAL'] ?? 0),
            'ESTOQ_MINIMO'  => (float)($r['ESTOQ_MINIMO'] ?? 0),
            'ESTOQUE_MAXIMO'=> (float)($r['ESTOQUE_MAXIMO'] ?? 0),
            'QTD_EMBALAGEM' => (float)($r['QTD_EMBALAGEM'] ?? 0),
            'CUSTO_FINAL'   => (float)($r['CUSTO_FINAL'] ?? 0),
            // Custo unitário p/ COMPRA = último custo de compra (o que se paga ao fornecedor).
            // Fallback: CUSTO_FINAL e depois CUSTO_MEDIO quando não há custo de compra.
            'CUSTO_UNIT'    => self::primeiroPositivo(
                                  (float)($r['CUSTO_COMPRA'] ?? 0),
                                  (float)($r['CUSTO_FINAL'] ?? 0),
                                  (float)($r['CUSTO_MEDIO'] ?? 0)
                               ),
            'PRECO_VENDA'   => (float)($r['PRECO_VENDA'] ?? 0),
            'MARGEM_PERC'   => null,             // preenchido abaixo
            'QTD_VENDIDA'   => $qtdVend,
            'FAT_VENDIDA'   => $fat,
            'VENDA_DIA'     => $qtdVend > 0 ? $qtdVend / $dias : 0.0,
            'VENDA_DIA_ANT' => 0.0,              // período anterior (p/ tendência)
            'TENDENCIA'     => 'flat',           // up | down | flat
            'CLASSE_ABC'    => 'C',              // A | B | C (relevância no faturamento)
            'FORNECEDOR_NOME' => '',
            'FORNECEDOR_COD'  => null,
            'ULT_CUSTO'       => 0.0,
            'ULT_COMPRA'      => null,
            'LEAD_TIME'       => 0,
            'SUGESTAO_QTD'    => 0.0,
            'SUGESTAO_CAIXAS' => 0,
            'VALOR_SUGERIDO'  => 0.0,
            'COBERTURA_DIAS'  => null,
            'URGENCIA'        => 0.0,
            'PRIORIDADE'      => 'observar',     // ja | repor | observar
            'ESTOQUE_SUSPEITO'=> false,
            'MOTIVO_RUPTURA'  => false,
            'MOTIVO_MINIMO'   => false,
            'MOTIVO_GIRO'     => false,
        ];
        // Margem % = (preço - custo) / preço. Usa o custo unitário efetivo (CUSTO_UNIT).
        if ($row['PRECO_VENDA'] > 0) {
            $row['MARGEM_PERC'] = round(($row['PRECO_VENDA'] - $row['CUSTO_UNIT']) / $row['PRECO_VENDA'] * 100, 1);
        }
        return $row;
    }

    /** Calcula sugestão de quantidade, motivos e urgência. $lead = dias de lead a somar (0 = ignora). */
    private function calcular(array &$c, int $cobertura, int $lead): void
    {
        $estoque  = $c['ESTOQUE_ATUAL'];
        $estoqueP = max(0.0, $estoque);          // físico: estoque negativo é trata como 0
        $min      = $c['ESTOQ_MINIMO'];
        $max      = $c['ESTOQUE_MAXIMO'];
        $vendaDia = $c['VENDA_DIA'];
        $c['LEAD_TIME'] = $lead;

        $c['COBERTURA_DIAS'] = $vendaDia > 0 ? round($estoque / $vendaDia, 1) : null;

        // Giro: cobrir (cobertura-alvo + lead) dias de venda, a partir do estoque físico.
        $alvoDias = $cobertura + $lead;
        $sugGiro  = $vendaDia > 0 ? max(0.0, $vendaDia * $alvoDias - $estoqueP) : 0.0;

        // Mínimo do ERP: se abaixo do mínimo, repor ao menos até o mínimo.
        $abaixoMin = ($min > 0 && $estoque < $min);
        $sugMin    = $abaixoMin ? max(0.0, $min - $estoqueP) : 0.0;

        $sug = max($sugGiro, $sugMin);

        // Respeita um máximo "real" (ignora o sentinela 100000 do cadastro).
        if ($max > 0 && $max < 90000 && $max > $min) {
            $sug = min($sug, max(0.0, $max - $estoqueP));
        }
        // Arredonda a sugestão PARA CIMA. Em múltiplos da embalagem (caixa/fardo) quando há;
        // senão, para UNIDADE inteira (compra-se em peças, e faz "Comprar × Custo = Valor" bater).
        $emb = $c['QTD_EMBALAGEM'];
        if ($emb >= 2 && $sug > 0) {
            $caixas = (int) ceil($sug / $emb);
            $sug    = $caixas * $emb;
            $c['SUGESTAO_CAIXAS'] = $caixas;
        } else {
            $sug = ceil($sug);
            $c['SUGESTAO_CAIXAS'] = 0;
        }

        $ruptura = ($estoque <= 0 && $vendaDia > 0);

        $c['SUGESTAO_QTD']   = $sug;
        $c['VALOR_SUGERIDO'] = $sug * $c['CUSTO_UNIT'];
        $c['MOTIVO_RUPTURA'] = $ruptura;
        $c['MOTIVO_MINIMO']  = $abaixoMin;
        $c['MOTIVO_GIRO']    = ($sugGiro > 0 && !$ruptura);
        $c['ESTOQUE_SUSPEITO'] = ($estoque < 0);  // furo de cadastro → conferir saldo

        // Faixa de prioridade p/ o comprador agir sem pensar:
        //   ja      = ruptura que vende (perdendo venda agora)
        //   repor   = vai acabar antes da cobertura-alvo (giro) ou abaixo do mínimo vendendo
        //   observar= o resto (abaixo do mínimo sem venda, folga)
        if ($ruptura) {
            $c['PRIORIDADE'] = 'ja';
        } elseif ($c['MOTIVO_GIRO'] || ($abaixoMin && $vendaDia > 0)) {
            $c['PRIORIDADE'] = 'repor';
        } else {
            $c['PRIORIDADE'] = 'observar';
        }

        // Urgência = R$/dia em risco (prioriza quem vende muito e está perto de zerar).
        $preco    = max($c['PRECO_VENDA'], $c['CUSTO_FINAL']);
        $riscoDia = $vendaDia * $preco;
        $cobEff   = $c['COBERTURA_DIAS'] !== null ? max(0.0, $c['COBERTURA_DIAS']) : null;
        $u = 0.0;
        if ($ruptura) {
            $u = $riscoDia * 3;
        } elseif ($vendaDia > 0 && $cobEff !== null && $cobEff < $cobertura) {
            $u = $riscoDia * (1 - $cobEff / $cobertura);
        }
        if ($abaixoMin) $u += 1; // desempate p/ abaixo-do-mínimo sem venda
        $c['URGENCIA'] = $u;
    }

    /** Monta resumo + agrupamento por fornecedor a partir das linhas finais. */
    private function montar(array $linhas, string $ini, string $fim, int $dias, int $cob, bool $comForn): array
    {
        $resumo = ['ITENS'=>0,'VALOR'=>0.0,'RUPTURAS'=>0,'ABAIXO_MIN'=>0,'FORNECEDORES'=>0];
        $porForn = [];
        foreach ($linhas as $c) {
            $resumo['ITENS']++;
            $resumo['VALOR'] += $c['VALOR_SUGERIDO'];
            if ($c['MOTIVO_RUPTURA']) $resumo['RUPTURAS']++;
            if ($c['MOTIVO_MINIMO'])  $resumo['ABAIXO_MIN']++;
            if ($comForn) {
                $fn = $c['FORNECEDOR_NOME'] ?: '(sem histórico de fornecedor)';
                if (!isset($porForn[$fn])) {
                    $porForn[$fn] = ['FORNECEDOR'=>$fn, 'FORNECEDOR_COD'=>$c['FORNECEDOR_COD'],
                                     'ITENS'=>0, 'VALOR'=>0.0, 'LEAD_TIME'=>$c['LEAD_TIME']];
                }
                $porForn[$fn]['ITENS']++;
                $porForn[$fn]['VALOR'] += $c['VALOR_SUGERIDO'];
            }
        }
        if ($comForn) {
            $resumo['FORNECEDORES'] = count($porForn);
            $porForn = array_values($porForn);
            usort($porForn, fn($a, $b) => $b['VALOR'] <=> $a['VALOR']);
        }
        return [
            'resumo'  => $resumo,
            'linhas'  => $linhas,
            'porForn' => $porForn,
            'dataIni' => $ini, 'dataFim' => $fim, 'dias' => $dias, 'cobertura' => $cob,
            'comFornecedor' => $comForn,
        ];
    }

    private function vazio(string $ini, string $fim, int $cob, int $dias): array
    {
        return ['resumo'=>['ITENS'=>0,'VALOR'=>0,'RUPTURAS'=>0,'ABAIXO_MIN'=>0,'FORNECEDORES'=>0],
                'linhas'=>[], 'porForn'=>[], 'dataIni'=>$ini, 'dataFim'=>$fim, 'dias'=>$dias,
                'cobertura'=>$cob, 'comFornecedor'=>false];
    }

    /** Primeiro valor > 0 da lista (p/ escolher o custo unitário mais confiável). */
    private static function primeiroPositivo(float ...$vals): float
    {
        foreach ($vals as $v) { if ($v > 0) return $v; }
        return 0.0;
    }

    private function utf($s): string
    {
        $s = (string)$s;
        if ($s !== '' && !mb_check_encoding($s, 'UTF-8')) {
            $s = mb_convert_encoding($s, 'UTF-8', 'Windows-1252');
        }
        return trim($s);
    }

    // ──────────────────────────────────────────────────────────────────────────
    // RASTREADOR DE RUPTURA  (tela /compras/ruptura)
    //
    // Tudo aqui usa as funções já cacheadas (sugestao + mapaFornecedor + mapaLead)
    // e só transforma a saída. Zero SQL nova — só "achatamos" o resultado pra
    // alimentar KPIs, ranking por venda perdida e ranking por marca.
    //
    // Depto de Engenharia e Automação - Master Automação - Autor: Derlivon Silva
    // ──────────────────────────────────────────────────────────────────────────

    /**
     * Pacote pronto pra tela de Ruptura:
     *   - linhas (itens críticos: rupturados OU abaixo do mínimo com venda)
     *   - kpis   (6 cartões do topo)
     *   - porMarca / topVendaPerdida (gráficos)
     *
     * Reusa sugestao() com filtros amplos e enriquece localmente.
     */
    public function dadosRuptura(array $f = []): array
    {
        $dataIni = $f['data_ini'] ?? date('Y-m-d', strtotime('-29 days'));
        $dataFim = $f['data_fim'] ?? $this->ultimaData();
        $cobertura = max(1, (int)($f['cobertura'] ?? 30));
        $leadPad   = max(0, (int)($f['lead_padrao'] ?? 7));

        // 1) Pega ruptura + abaixo do mínimo num único request (com fornecedor)
        $base = $this->sugestao([
            'data_ini'          => $dataIni,
            'data_fim'          => $dataFim,
            'cobertura'         => $cobertura,
            'lead_padrao'       => $leadPad,
            'apenas'            => '',           // não filtra; filtramos abaixo
            'incluir_sem_venda' => true,
            'com_fornecedor'    => true,
            'usar_lead'         => true,
            'ordenar'           => 'urgencia',
            'limite'            => 1500,
        ]);

        $linhas = $base['linhas'] ?? [];

        // 2) Filtro de "itens críticos" = ruptura efetiva OU abaixo do mínimo com venda
        $criticos = array_values(array_filter($linhas, function ($c) {
            return !empty($c['MOTIVO_RUPTURA'])
                || (!empty($c['MOTIVO_MINIMO']) && (float)($c['VENDA_DIA'] ?? 0) > 0);
        }));

        // 3) Anexa marca (COD_MARCA + nome) — leve, cache 24h.
        // Fix 24/06/2026: passa SÓ os COD_ITEM críticos pro mapaMarca, evita
        // materializar a tabela ITENS inteira em memória (E_ERROR memory_limit
        // em cliente Real Prime — dezenas de milhares de SKUs históricos).
        $idsCriticos = array_map(fn($c) => (int)($c['COD_ITEM'] ?? 0), $criticos);
        $marcas = $this->mapaMarca($idsCriticos);
        foreach ($criticos as &$c) {
            $m = $marcas[$c['COD_ITEM']] ?? null;
            $c['COD_MARCA']  = $m['cod']  ?? null;
            $c['MARCA_NOME'] = $m['nome'] ?? '';
            // Venda perdida diária = unidades/dia × preço de venda (R$/dia em risco)
            $preco = (float)($c['PRECO_VENDA'] ?? 0);
            if ($preco <= 0) $preco = (float)($c['CUSTO_UNIT'] ?? 0); // fallback
            $c['VENDA_PERDIDA_DIA'] = max(0.0, (float)($c['VENDA_DIA'] ?? 0) * $preco);
            $c['EFETIVO_RUPTURA']   = !empty($c['MOTIVO_RUPTURA']);
        }
        unset($c);

        // Ordena pelos mais críticos primeiro: ruptura efetiva > urgência > venda perdida
        usort($criticos, function ($a, $b) {
            $aR = $a['EFETIVO_RUPTURA'] ? 1 : 0;
            $bR = $b['EFETIVO_RUPTURA'] ? 1 : 0;
            if ($aR !== $bR) return $bR - $aR;
            $au = (float)($a['URGENCIA'] ?? 0);
            $bu = (float)($b['URGENCIA'] ?? 0);
            if ($au !== $bu) return $bu <=> $au;
            return ($b['VENDA_PERDIDA_DIA'] ?? 0) <=> ($a['VENDA_PERDIDA_DIA'] ?? 0);
        });

        // 4) KPIs do topo
        $kpis = $this->montarKpisRuptura($criticos, $linhas);

        // 5) Top 20 por venda perdida
        $top = array_slice($criticos, 0, 20);
        $topVendaPerdida = array_map(fn($c) => [
            'COD_ITEM'           => $c['COD_ITEM'],
            'DESCRICAO'          => $c['DESCRICAO'],
            'VENDA_PERDIDA_DIA'  => round($c['VENDA_PERDIDA_DIA'], 2),
        ], $top);
        usort($topVendaPerdida, fn($a, $b) => $b['VENDA_PERDIDA_DIA'] <=> $a['VENDA_PERDIDA_DIA']);

        // 6) Ranking por marca (substituto do "categoria" — o ERP Fire usa MARCA como agrupador comercial)
        $porMarca = [];
        foreach ($criticos as $c) {
            $k = $c['MARCA_NOME'] !== '' ? $c['MARCA_NOME'] : '(sem marca)';
            if (!isset($porMarca[$k])) {
                $porMarca[$k] = ['MARCA' => $k, 'ITENS' => 0, 'VENDA_PERDIDA_DIA' => 0.0, 'VALOR_SUGERIDO' => 0.0];
            }
            $porMarca[$k]['ITENS']++;
            $porMarca[$k]['VENDA_PERDIDA_DIA'] += $c['VENDA_PERDIDA_DIA'];
            $porMarca[$k]['VALOR_SUGERIDO']    += (float)($c['VALOR_SUGERIDO'] ?? 0);
        }
        $porMarca = array_values($porMarca);
        usort($porMarca, fn($a, $b) => $b['VENDA_PERDIDA_DIA'] <=> $a['VENDA_PERDIDA_DIA']);
        $porMarca = array_slice($porMarca, 0, 20);

        return [
            'kpis'             => $kpis,
            'linhas'           => $criticos,
            'topVendaPerdida'  => $topVendaPerdida,
            'porMarca'         => $porMarca,
            'dataIni'          => $dataIni,
            'dataFim'          => $dataFim,
            'dias'             => (int)($base['dias'] ?? 30),
            'cobertura'        => $cobertura,
        ];
    }

    /**
     * 6 KPIs do topo da tela.
     * - qtd_ruptura      : SKUs efetivamente zerados que vendem (estoque <= 0 e venda > 0)
     * - venda_perdida_dia: soma R$/dia em risco
     * - cobertura_media  : mediana de cobertura dos 50 SKUs que mais vendem (mais robusta que média)
     * - skus_a_risco     : itens classe A com cobertura < lead time
     * - lead_time_medio  : média de lead time dos itens em ruptura
     * - pct_ruptura      : qtd ruptura / total SKUs com venda no período
     */
    private function montarKpisRuptura(array $criticos, array $todos): array
    {
        $rupt = array_filter($criticos, fn($c) => !empty($c['EFETIVO_RUPTURA']));
        $qtdRuptura = count($rupt);
        $vendaPerdida = 0.0;
        foreach ($rupt as $c) { $vendaPerdida += (float)($c['VENDA_PERDIDA_DIA'] ?? 0); }

        // Cobertura mediana dos top 50 por venda (entre TODOS, não só críticos)
        $comVenda = array_filter($todos, fn($c) => (float)($c['VENDA_DIA'] ?? 0) > 0);
        usort($comVenda, fn($a, $b) => ($b['VENDA_DIA'] ?? 0) <=> ($a['VENDA_DIA'] ?? 0));
        $top50 = array_slice($comVenda, 0, 50);
        $cobs = [];
        foreach ($top50 as $c) {
            if ($c['COBERTURA_DIAS'] !== null) $cobs[] = max(0.0, (float)$c['COBERTURA_DIAS']);
        }
        sort($cobs);
        $coberturaMedia = 0.0;
        if ($cobs) {
            $n = count($cobs);
            $coberturaMedia = ($n % 2) ? $cobs[(int)($n/2)] : (($cobs[$n/2 - 1] + $cobs[$n/2]) / 2);
        }

        // Classe A em risco (cobertura < lead time do próprio item, ou < 7 se não tem)
        $skusARisco = 0;
        foreach ($criticos as $c) {
            if (($c['CLASSE_ABC'] ?? '') !== 'A') continue;
            $lt = (int)($c['LEAD_TIME'] ?? 0); if ($lt <= 0) $lt = 7;
            $cb = $c['COBERTURA_DIAS'];
            if ($cb === null || $cb < $lt) $skusARisco++;
        }

        // Lead time médio (só dos rupturados)
        $leadTimes = [];
        foreach ($rupt as $c) {
            $lt = (int)($c['LEAD_TIME'] ?? 0);
            if ($lt > 0) $leadTimes[] = $lt;
        }
        $leadTimeMedio = $leadTimes ? round(array_sum($leadTimes) / count($leadTimes), 1) : 0;

        // % ruptura sobre SKUs ativos (com venda)
        $totalAtivos = count($comVenda);
        $pctRuptura = $totalAtivos > 0 ? round($qtdRuptura / $totalAtivos * 100, 1) : 0.0;

        return [
            'qtd_ruptura'        => $qtdRuptura,
            'venda_perdida_dia'  => round($vendaPerdida, 2),
            'cobertura_media'    => round($coberturaMedia, 1),
            'skus_a_risco'       => $skusARisco,
            'lead_time_medio'    => $leadTimeMedio,
            'pct_ruptura'        => $pctRuptura,
            'total_ativos'       => $totalAtivos,
        ];
    }

    /**
     * cod_item => {cod, nome} da MARCA.
     *
     * IMPORTANTE (fix 24/06/2026): SELECT escopado:
     *   - SEMPRE filtra por COD_EMPRESA (via ITENS_ESTOQUE) — antes pegava
     *     ITENS inteira (dezenas de milhares de SKUs históricos) e estourava
     *     memory_limit=128M num fetchAll() em cliente grande tipo Real Prime.
     *   - Quando $codItens != [], restringe ainda mais a esses COD_ITEM
     *     específicos (vindos de $criticos) — corte de ~30k pra ~1.5k linhas.
     *   - foreach direto no statement (sem fetchAll) pra não materializar tudo.
     * Cache 24h por chave (emp + hash dos ids).
     */
    private function mapaMarca(array $codItens = []): array
    {
        $emp = $this->codEmpresa;

        // Normaliza lista de ids (int único, sem zeros/duplicatas)
        $ids = [];
        foreach ($codItens as $v) {
            $i = (int)$v;
            if ($i > 0) $ids[$i] = true;
        }
        $ids = array_keys($ids);
        sort($ids);

        $ck  = 'sugestao_marca_' . $emp . '_' . ($ids ? substr(md5(implode(',', $ids)), 0, 12) . '_n' . count($ids) : 'all');

        return Cache::lembrar($ck, 86400, function () use ($emp, $ids) {
            $out = [];
            try {
                if ($ids) {
                    // Firebird aceita IN com muitos params, mas pra segurança quebramos em chunks de 1000
                    foreach (array_chunk($ids, 1000) as $chunk) {
                        $place = implode(',', array_fill(0, count($chunk), '?'));
                        $sql = "SELECT i.COD_ITEM, i.COD_MARCA, TRIM(COALESCE(m.DESCRICAO,'')) NOME
                                FROM ITENS i
                                LEFT JOIN MARCA m ON m.COD_MARCA = i.COD_MARCA
                                WHERE i.COD_ITEM IN ({$place})";
                        $st = $this->pdo->prepare($sql);
                        $st->execute($chunk);
                        // foreach direto no statement (sem fetchAll → não materializa)
                        while ($r = $st->fetch(\PDO::FETCH_ASSOC)) {
                            $out[(int)$r['COD_ITEM']] = [
                                'cod'  => $r['COD_MARCA'] !== null ? (int)$r['COD_MARCA'] : null,
                                'nome' => $this->utf($r['NOME'] ?? ''),
                            ];
                        }
                        $st->closeCursor();
                    }
                } else {
                    // Fallback: escopa por empresa via EXISTS em ITENS_ESTOQUE.
                    // Reduz de "ITENS inteira" (catálogo histórico) pros itens
                    // realmente cadastrados na empresa ativa.
                    $sql = "SELECT i.COD_ITEM, i.COD_MARCA, TRIM(COALESCE(m.DESCRICAO,'')) NOME
                            FROM ITENS i
                            LEFT JOIN MARCA m ON m.COD_MARCA = i.COD_MARCA
                            WHERE EXISTS (
                                SELECT 1 FROM ITENS_ESTOQUE ie
                                WHERE ie.COD_ITEM = i.COD_ITEM
                                  AND ie.COD_EMPRESA = {$emp}
                            )";
                    $st = $this->pdo->query($sql);
                    while ($r = $st->fetch(\PDO::FETCH_ASSOC)) {
                        $out[(int)$r['COD_ITEM']] = [
                            'cod'  => $r['COD_MARCA'] !== null ? (int)$r['COD_MARCA'] : null,
                            'nome' => $this->utf($r['NOME'] ?? ''),
                        ];
                    }
                    $st->closeCursor();
                }
            } catch (\Throwable) { /* sem MARCA → vazio, graceful degrade */ }
            return $out;
        });
    }
}
