<?php
declare(strict_types=1);

namespace Celena\Plugin\Analytics\Support;

use Celena\Plugin\Analytics\Mmdb\Reader;

/**
 * Загрузка/обновление базы город+координаты DB-IP City Lite (.mmdb, CC-BY).
 * Качает gz за текущий месяц (при неудаче — за прошлый), распаковывает в
 * storage/analytics/dbip-city.mmdb. Атомарно: пишем во временный файл, проверяем
 * читаемость как MMDB и только потом подменяем боевой. Запуск вручную/по крону:
 *   php plugins/analytics/tools/import-geo-city.php
 */
final class GeoCityImporter
{
    /** @return array{file:string,size:int,url:string} */
    public function import(?string $url = null, ?callable $log = null): array
    {
        $log = $log ?? static fn (string $m) => null;
        $dest = GeoCity::dbPath();
        $dir = dirname($dest);
        if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) {
            throw new \RuntimeException("Не создать каталог $dir");
        }

        $candidates = $url !== null ? [$url] : [
            'https://download.db-ip.com/free/dbip-city-lite-' . date('Y-m') . '.mmdb.gz',
            'https://download.db-ip.com/free/dbip-city-lite-' . date('Y-m', mktime(0, 0, 0, (int) date('n') - 1, 1, (int) date('Y'))) . '.mmdb.gz',
        ];

        $tmpGz = tempnam(sys_get_temp_dir(), 'dbipcity') ?: ($dir . '/dl.gz');
        $used = $this->download($candidates, $tmpGz, $log);

        $log('Распаковка…');
        $tmpDb = $dest . '.tmp';
        $this->gunzip($tmpGz, $tmpDb);
        @unlink($tmpGz);

        // Проверка: файл реально читается как MMDB
        try {
            new Reader($tmpDb);
        } catch (\Throwable $e) {
            @unlink($tmpDb);
            throw new \RuntimeException('Скачанный файл не читается как MMDB: ' . $e->getMessage());
        }

        if (!@rename($tmpDb, $dest)) {
            @unlink($tmpDb);
            throw new \RuntimeException("Не подменить $dest");
        }
        @chmod($dest, 0644);

        return ['file' => $dest, 'size' => (int) filesize($dest), 'url' => $used];
    }

    private function gunzip(string $src, string $dst): void
    {
        $in = gzopen($src, 'rb');
        $out = fopen($dst, 'wb');
        if ($in === false || $out === false) {
            if ($in !== false) gzclose($in);
            if ($out !== false) fclose($out);
            throw new \RuntimeException('Не открыть файлы для распаковки');
        }
        while (!gzeof($in)) {
            $chunk = gzread($in, 1 << 20);
            if ($chunk === false) break;
            fwrite($out, $chunk);
        }
        gzclose($in);
        fclose($out);
    }

    /** @param string[] $urls */
    private function download(array $urls, string $dest, callable $log): string
    {
        if (!function_exists('curl_init')) {
            throw new \RuntimeException('curl недоступен');
        }
        $lastErr = '';
        foreach ($urls as $u) {
            $log("Скачиваю $u …");
            $fp = fopen($dest, 'wb');
            if ($fp === false) {
                throw new \RuntimeException("Не открыть $dest для записи");
            }
            $ch = curl_init($u);
            curl_setopt_array($ch, [
                CURLOPT_FILE           => $fp,
                CURLOPT_FOLLOWLOCATION => true,
                CURLOPT_USERAGENT      => 'Mozilla/5.0 (CelenaAnalytics)',
                CURLOPT_TIMEOUT        => 600,
                CURLOPT_FAILONERROR    => true,
            ]);
            $ok = curl_exec($ch);
            $err = curl_error($ch);
            $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
            curl_close($ch);
            fclose($fp);
            if ($ok !== false && $code < 400) {
                return $u;
            }
            $lastErr = $err !== '' ? $err : ('HTTP ' . $code);
            $log("  не вышло: $lastErr");
            @unlink($dest);
        }
        throw new \RuntimeException('Не удалось скачать базу DB-IP City: ' . $lastErr);
    }
}
