<?php
declare(strict_types=1);

/**
 * Arquivos recebidos: gravação na pasta privada, registro no banco, listagem, download e exclusão.
 *
 * Cada envio guarda um arquivo só: o tratado ("pronto"), com as colunas padronizadas, o que o aliado
 * aceitou completar e Unidade/Segmento/Família completados. É o que vai para o dashboard. O arquivo
 * como o aliado mandou não fica guardado (envios antigos podem ainda ter o "_original").
 * Quando um aliado reenvia o mesmo tipo e mês, o envio anterior fica marcado como substituído.
 */
final class Envios
{
    public const TIPOS = ['sellout' => 'Sellout', 'estoque' => 'Estoque'];

    /** Grava o envio aceito (só o arquivo tratado). Devolve o id. */
    public static function grava(array $aliado, string $empresa, string $tipo, array $res, string $nomeOriginal): int
    {
        $mes = $res['resumo']['mes_ref'] !== '' ? $res['resumo']['mes_ref'] : 'sem-mes';
        $dir = Config::dirDados() . '/arquivos/' . $mes;
        if (!is_dir($dir) && !@mkdir($dir, 0750, true) && !is_dir($dir)) {
            throw new RuntimeException('Não consegui criar a pasta dos arquivos.');
        }
        $base = $tipo . '_' . Texto::nomeDeArquivo((string) $aliado['codigo'], 20) . '_' . $mes . '_' . date('Ymd-His') . '_' . bin2hex(random_bytes(3));
        $pronto = $dir . '/' . $base . '.xlsx';

        $saida = $res['saida'];
        XlsxGravador::grava($pronto, $saida['aba'], $saida['cabecalho'], $saida['linhas']);
        @chmod($pronto, 0640);

        $pdo = Banco::pdo();
        $pdo->beginTransaction();
        $st = $pdo->prepare('INSERT INTO envios (criado_em, aliado_codigo, aliado_nome, empresa_digitada, tipo, mes_ref, meses,
                linhas, total_usd, quantidade, clientes, itens, completados, sem_classificacao, codigos_fora,
                nome_original, arquivo, arquivo_original, tamanho, ip, completou)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
        $r = $res['resumo'];
        $st->execute([
            Banco::agora(), $aliado['codigo'], $aliado['nome'], $empresa, $tipo, $mes,
            json_encode($r['meses']), $r['linhas'], $r['total_usd'], $r['quantidade'], $r['clientes'], $r['itens'],
            $res['classif']['completados'], $res['classif']['sem_classificacao'], json_encode($res['classif']['codigos_fora']),
            mb_substr($nomeOriginal, 0, 200), self::relativo($pronto), '', (int) filesize($pronto),
            Seguranca::ip(), json_encode($res['completou'] ?? [], JSON_UNESCAPED_UNICODE),
        ]);
        $id = (int) $pdo->lastInsertId();
        self::recalculaVigentes((string) $aliado['codigo'], $tipo, $mes);
        $pdo->commit();
        return $id;
    }

    /** Marca como substituídos os envios mais antigos do mesmo aliado, tipo e mês. */
    public static function recalculaVigentes(string $aliado, string $tipo, string $mes): void
    {
        $pdo = Banco::pdo();
        $st = $pdo->prepare('SELECT id FROM envios WHERE aliado_codigo = ? AND tipo = ? AND mes_ref = ? ORDER BY criado_em DESC, id DESC');
        $st->execute([$aliado, $tipo, $mes]);
        $ids = $st->fetchAll(PDO::FETCH_COLUMN);
        $up = $pdo->prepare('UPDATE envios SET substituido = ? WHERE id = ?');
        foreach ($ids as $i => $id) {
            $up->execute([$i === 0 ? 0 : 1, $id]);
        }
    }

    /** Envio vigente (o último) do aliado para o tipo e o mês, ou null. */
    public static function vigente(string $aliado, string $tipo, string $mes): ?array
    {
        $st = Banco::pdo()->prepare('SELECT * FROM envios WHERE aliado_codigo = ? AND tipo = ? AND mes_ref = ? AND substituido = 0
            ORDER BY criado_em DESC, id DESC LIMIT 1');
        $st->execute([$aliado, $tipo, $mes]);
        $r = $st->fetch();
        return $r === false ? null : $r;
    }

    /** Últimos envios de um aliado (para ele mesmo ver). */
    public static function doAliado(string $aliado, int $limite = 24): array
    {
        $st = Banco::pdo()->prepare('SELECT * FROM envios WHERE aliado_codigo = ? ORDER BY criado_em DESC, id DESC LIMIT ' . (int) $limite);
        $st->execute([$aliado]);
        return $st->fetchAll();
    }

    /** Guarda o resumo de um arquivo recusado, para o administrador ajudar o aliado. */
    public static function registraRecusa(string $aliado, string $tipo, string $nomeOriginal, array $erros): void
    {
        $pdo = Banco::pdo();
        $pdo->prepare('DELETE FROM recusas WHERE criado_em < ?')->execute([date('Y-m-d H:i:s', strtotime('-180 days'))]);
        $titulos = array_slice(array_column($erros, 'titulo'), 0, 8);
        $pdo->prepare('INSERT INTO recusas (criado_em, aliado_codigo, tipo, nome_original, erros) VALUES (?, ?, ?, ?, ?)')
            ->execute([Banco::agora(), $aliado, $tipo, mb_substr($nomeOriginal, 0, 200), json_encode($titulos, JSON_UNESCAPED_UNICODE)]);
    }

    /** Recusas dos últimos dias, com o nome do aliado. */
    public static function recusas(int $dias = 30, int $limite = 60): array
    {
        $st = Banco::pdo()->prepare('SELECT r.*, a.nome AS aliado_nome FROM recusas r LEFT JOIN aliados a ON a.codigo = r.aliado_codigo
            WHERE r.criado_em >= ? ORDER BY r.criado_em DESC, r.id DESC LIMIT ' . (int) $limite);
        $st->execute([date('Y-m-d H:i:s', strtotime('-' . $dias . ' days'))]);
        return $st->fetchAll();
    }

    /**
     * Códigos que não estão na tabela de preços, juntando os envios da lista.
     * @return list<array{codigo:string, descricao:string, aliados:list<string>, envios:int}>
     */
    public static function codigosFora(array $lista): array
    {
        $out = [];
        foreach ($lista as $e) {
            $codigos = json_decode((string) $e['codigos_fora'], true);
            if (!is_array($codigos)) {
                continue;
            }
            foreach ($codigos as $c) {
                [$cod, $desc] = is_array($c) ? [(string) ($c[0] ?? ''), (string) ($c[1] ?? '')] : [(string) $c, ''];
                if ($cod === '') {
                    continue;
                }
                $out[$cod] ??= ['codigo' => $cod, 'descricao' => $desc, 'aliados' => [], 'envios' => 0];
                if ($out[$cod]['descricao'] === '' && $desc !== '') {
                    $out[$cod]['descricao'] = $desc;
                }
                $out[$cod]['aliados'][(string) $e['aliado_nome']] = true;
                $out[$cod]['envios']++;
            }
        }
        foreach ($out as &$o) {
            $o['aliados'] = array_keys($o['aliados']);
        }
        unset($o);
        uasort($out, static fn ($a, $b) => [$b['envios'], $a['codigo']] <=> [$a['envios'], $b['codigo']]);
        return array_values($out);
    }

    public static function busca(int $id): ?array
    {
        $st = Banco::pdo()->prepare('SELECT * FROM envios WHERE id = ?');
        $st->execute([$id]);
        $r = $st->fetch();
        return $r === false ? null : $r;
    }

    /**
     * @param array{mes?:string, aliado?:string, tipo?:string, substituidos?:bool} $f
     * @return list<array<string,mixed>>
     */
    public static function lista(array $f, int $limite = 500): array
    {
        $onde = [];
        $par = [];
        if (!empty($f['mes'])) {
            $onde[] = 'mes_ref = ?';
            $par[] = $f['mes'];
        }
        if (!empty($f['aliado'])) {
            $onde[] = 'aliado_codigo = ?';
            $par[] = $f['aliado'];
        }
        if (!empty($f['tipo'])) {
            $onde[] = 'tipo = ?';
            $par[] = $f['tipo'];
        }
        if (empty($f['substituidos'])) {
            $onde[] = 'substituido = 0';
        }
        $sql = 'SELECT * FROM envios' . ($onde ? ' WHERE ' . implode(' AND ', $onde) : '') . ' ORDER BY criado_em DESC, id DESC LIMIT ' . (int) $limite;
        $st = Banco::pdo()->prepare($sql);
        $st->execute($par);
        return $st->fetchAll();
    }

    /** Meses com envios, do mais recente ao mais antigo. @return list<string> */
    public static function meses(): array
    {
        return Banco::pdo()->query('SELECT DISTINCT mes_ref FROM envios ORDER BY mes_ref DESC')->fetchAll(PDO::FETCH_COLUMN);
    }

    /** Envios vigentes de um mês, por aliado e tipo: [código][tipo] => envio. */
    public static function situacaoDoMes(string $mes): array
    {
        $st = Banco::pdo()->prepare('SELECT * FROM envios WHERE mes_ref = ? AND substituido = 0');
        $st->execute([$mes]);
        $out = [];
        foreach ($st->fetchAll() as $r) {
            $out[$r['aliado_codigo']][$r['tipo']] = $r;
        }
        return $out;
    }

    public static function exclui(int $id): bool
    {
        $e = self::busca($id);
        if ($e === null) {
            return false;
        }
        foreach (['arquivo', 'arquivo_original'] as $k) {
            $p = self::absoluto((string) $e[$k]);
            if ($p !== null && is_file($p)) {
                @unlink($p);
            }
        }
        Banco::pdo()->prepare('DELETE FROM envios WHERE id = ?')->execute([$id]);
        self::recalculaVigentes((string) $e['aliado_codigo'], (string) $e['tipo'], (string) $e['mes_ref']);
        return true;
    }

    /** Nome amigável para o download: Sellout_2026-10_1014066_FULLVISION.xlsx */
    public static function nomeAmigavel(array $e, bool $original = false): string
    {
        $nome = (self::TIPOS[$e['tipo']] ?? 'Arquivo') . '_' . $e['mes_ref'] . '_' . Texto::nomeDeArquivo((string) $e['aliado_codigo'], 20)
            . '_' . Texto::nomeDeArquivo((string) $e['aliado_nome'], 30);
        if ($original) {
            $ext = strtolower(pathinfo((string) $e['arquivo_original'], PATHINFO_EXTENSION));
            return $nome . '_original.' . ($ext === 'xlsm' ? 'xlsm' : 'xlsx');
        }
        return $nome . '.xlsx';
    }

    /** Envia um arquivo para download (só a área administrativa chama). O original só existe em envios antigos. */
    public static function baixa(array $e, bool $original): never
    {
        $p = self::absoluto((string) ($original ? $e['arquivo_original'] : $e['arquivo']));
        if ($p === null || !is_file($p)) {
            http_response_code(404);
            exit('Arquivo não encontrado.');
        }
        $ext = strtolower(pathinfo($p, PATHINFO_EXTENSION));
        self::entrega($p, self::nomeAmigavel($e, $original), $ext === 'xlsm'
            ? 'application/vnd.ms-excel.sheet.macroEnabled.12'
            : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
    }

    /** Monta um .zip com os arquivos prontos dos envios da lista e envia. */
    public static function baixaZip(array $lista, string $nomeZip): never
    {
        $tmp = tempnam(sys_get_temp_dir(), 'zp');
        $zip = new ZipArchive();
        $zip->open($tmp, ZipArchive::OVERWRITE);
        $usados = [];
        foreach ($lista as $e) {
            $p = self::absoluto((string) $e['arquivo']);
            if ($p === null || !is_file($p)) {
                continue;
            }
            $nome = self::nomeAmigavel($e);
            if (isset($usados[$nome])) {
                $nome = preg_replace('/\.xlsx$/', '_' . $e['id'] . '.xlsx', $nome);
            }
            $usados[$nome] = true;
            $zip->addFile($p, $nome);
        }
        if (!$usados) {
            $zip->addFromString('LEIA-ME.txt', "Nenhum arquivo com esses filtros.\r\n");
        }
        $zip->close();
        self::entrega($tmp, $nomeZip, 'application/zip', true);
    }

    /** Manda um arquivo para o navegador baixar (e apaga depois, se for temporário). */
    public static function entrega(string $caminho, string $nome, string $tipo, bool $apagarDepois = false): never
    {
        if ($apagarDepois) {
            register_shutdown_function(static fn () => @unlink($caminho));
        }
        while (ob_get_level() > 0) {
            ob_end_clean();
        }
        header('Content-Type: ' . $tipo);
        header('Content-Length: ' . filesize($caminho));
        $ascii = preg_replace('/[^A-Za-z0-9._-]+/', '_', Texto::semAcento($nome)) ?? 'arquivo';
        header('Content-Disposition: attachment; filename="' . $ascii . '"; filename*=UTF-8\'\'' . rawurlencode($nome));
        header('X-Content-Type-Options: nosniff');
        header('Cache-Control: no-store');
        readfile($caminho);
        exit;
    }

    private static function relativo(string $caminho): string
    {
        $base = Config::dirDados() . '/';
        return str_starts_with($caminho, $base) ? substr($caminho, strlen($base)) : $caminho;
    }

    /** Caminho absoluto de um arquivo guardado, sempre dentro da pasta de dados. */
    private static function absoluto(string $relativo): ?string
    {
        if ($relativo === '' || str_contains($relativo, '..')) {
            return null;
        }
        return Config::dirDados() . '/' . ltrim($relativo, '/');
    }
}
