<?php
declare(strict_types=1);

namespace Celena\Plugin\Analytics\Support;

use Celena\Core\Database\Connection;

/**
 * Импорт бесплатной базы DB-IP IP-to-Country Lite (CC-BY) в cl_geo_ip4.
 * Только IPv4-диапазоны. Запускается вручную/по крону:
 *   php plugins/analytics/tools/import-geo.php
 * Атомарно: при ошибке старые данные сохраняются (всё в одной транзакции).
 */
final class GeoImporter
{
    private const BATCH = 1000;

    public function __construct(private readonly Connection $conn)
    {
    }

    /** @return array{rows:int,url:string} */
    public function import(?string $url = null, ?callable $log = null): array
    {
        $log = $log ?? static fn (string $m) => null;
        $tmp = tempnam(sys_get_temp_dir(), 'dbip');

        $url = $this->resolveAndDownload($url, $tmp, $log);
        $gz = gzopen($tmp, 'rb');
        if ($gz === false) {
            @unlink($tmp);
            throw new \RuntimeException('Не удалось открыть gz-файл базы');
        }

        $T = $this->conn->table_q('geo_ip4');
        $colS = $this->conn->quoteId('start_int');
        $colE = $this->conn->quoteId('end_int');
        $colC = $this->conn->quoteId('cc');

        $rows = $this->conn->transaction(function () use ($gz, $T, $colS, $colE, $colC, $log): int {
            $this->conn->exec('DELETE FROM ' . $T); // полное обновление
            $batch = [];
            $count = 0;
            while (($line = gzgets($gz)) !== false) {
                $line = trim($line);
                if ($line === '') continue;
                $p = explode(',', $line);
                if (count($p) < 3) continue;
                if (str_contains($p[0], ':')) continue; // только IPv4
                $cc = strtoupper(trim($p[2]));
                if ($cc === 'ZZ' || !preg_match('/^[A-Z]{2}$/', $cc)) continue;
                $s = ip2long($p[0]);
                $e = ip2long($p[1]);
                if ($s === false || $e === false) continue;
                $batch[] = [(int) sprintf('%u', $s), (int) sprintf('%u', $e), $cc];
                if (count($batch) >= self::BATCH) {
                    $this->flush($T, $colS, $colE, $colC, $batch);
                    $count += count($batch);
                    $batch = [];
                    if ($count % 50000 === 0) $log("  $count диапазонов…");
                }
            }
            if ($batch !== []) {
                $this->flush($T, $colS, $colE, $colC, $batch);
                $count += count($batch);
            }
            return $count;
        });

        gzclose($gz);
        @unlink($tmp);
        return ['rows' => $rows, 'url' => $url];
    }

    private function flush(string $T, string $colS, string $colE, string $colC, array $batch): void
    {
        $ph = implode(',', array_fill(0, count($batch), '(?,?,?)'));
        $vals = [];
        foreach ($batch as $r) { $vals[] = $r[0]; $vals[] = $r[1]; $vals[] = $r[2]; }
        $this->conn->run("INSERT INTO $T ($colS,$colE,$colC) VALUES $ph", $vals);
    }

    /** Скачать базу за текущий месяц, при неудаче — за прошлый. Возвращает использованный URL. */
    private function resolveAndDownload(?string $url, string $dest, callable $log): string
    {
        $candidates = $url !== null ? [$url] : [
            'https://download.db-ip.com/free/dbip-country-lite-' . date('Y-m') . '.csv.gz',
            'https://download.db-ip.com/free/dbip-country-lite-' . date('Y-m', mktime(0, 0, 0, (int) date('n') - 1, 1, (int) date('Y'))) . '.csv.gz',
        ];
        $lastErr = '';
        foreach ($candidates as $u) {
            $log("Скачиваю $u …");
            try {
                $this->download($u, $dest);
                return $u;
            } catch (\Throwable $e) {
                $lastErr = $e->getMessage();
                $log("  не вышло: $lastErr");
            }
        }
        @unlink($dest);
        throw new \RuntimeException('Не удалось скачать базу DB-IP: ' . $lastErr);
    }

    private function download(string $url, string $dest): void
    {
        if (!function_exists('curl_init')) {
            throw new \RuntimeException('curl недоступен');
        }
        $fp = fopen($dest, 'wb');
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_FILE           => $fp,
            CURLOPT_FOLLOWLOCATION => true,
            CURLOPT_USERAGENT      => 'Mozilla/5.0 (CelenaAnalytics)',
            CURLOPT_TIMEOUT        => 180,
            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) {
            throw new \RuntimeException($err !== '' ? $err : ('HTTP ' . $code));
        }
    }
}
