<?php
/**
 * IP → 国家码跨请求缓存（Redis 优先，文件降级，默认 TTL 30 天）
 */
require_once __DIR__ . '/CountryCodeNormalizer.php';

class GeoIpCountryCache
{
    const REDIS_PREFIX = 'cloak:geoip:cc:';
    const NONE_SENTINEL = '__NONE__';

    /** @var bool|null */
    private static $redisAvailable = null;

    /**
     * @param string $ip
     * @return string|false|null 命中返回国家码或 null（已知无国家）；false=未命中
     */
    public static function get($ip)
    {
        $ip = ClientIpResolver::normalizeIp($ip);
        if ($ip === null) {
            return false;
        }

        $redis = self::redisClient();
        if ($redis) {
            try {
                $value = $redis->get(self::REDIS_PREFIX . $ip);
                if ($value === false || $value === null) {
                    return false;
                }
                return self::normalizeCachedValue($value === self::NONE_SENTINEL ? null : (string) $value);
            } catch (Throwable $e) {
                self::logError('Redis get failed: ' . $e->getMessage());
            }
        }

        return self::getFile($ip);
    }

    /**
     * @param string      $ip
     * @param string|null $countryCode
     */
    public static function set($ip, $countryCode)
    {
        $ip = ClientIpResolver::normalizeIp($ip);
        if ($ip === null) {
            return;
        }

        if ($countryCode !== null && $countryCode !== '') {
            $normalized = CountryCodeNormalizer::toAlpha2($countryCode);
            $value = $normalized !== null ? $normalized : strtoupper($countryCode);
        } else {
            $value = self::NONE_SENTINEL;
        }
        $ttl   = self::cacheTtl();

        $redis = self::redisClient();
        if ($redis) {
            try {
                $redis->setex(self::REDIS_PREFIX . $ip, $ttl, $value);
                return;
            } catch (Throwable $e) {
                self::logError('Redis set failed: ' . $e->getMessage());
            }
        }

        self::setFile($ip, $value, $ttl);
    }

    private static function cacheTtl()
    {
        $ttl = defined('CLOAK_GEOIP_CACHE_TTL') ? (int) CLOAK_GEOIP_CACHE_TTL : 2592000;
        return max(60, $ttl);
    }

    /**
     * @return Redis|null
     */
    private static function redisClient()
    {
        if (self::$redisAvailable === false) {
            return null;
        }
        if (!extension_loaded('redis') || !defined('REDIS_ENABLED') || strtoupper(REDIS_ENABLED) !== 'ON') {
            self::$redisAvailable = false;
            return null;
        }
        try {
            $r = new Redis();
            $timeout = defined('REDIS_TIMEOUT') ? (float) REDIS_TIMEOUT : 1.0;
            if (!@$r->connect(REDIS_HOST, (int) REDIS_PORT, $timeout)) {
                self::$redisAvailable = false;
                return null;
            }
            if (defined('REDIS_PASSWORD') && REDIS_PASSWORD !== '') {
                $r->auth(REDIS_PASSWORD);
            }
            self::$redisAvailable = true;
            return $r;
        } catch (Throwable $e) {
            self::$redisAvailable = false;
            return null;
        }
    }

    private static function cacheDir()
    {
        $dir = dirname(__DIR__, 2) . '/storage/runtime/c7f2a9e1/cc_cache';
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }
        return $dir;
    }

    private static function cacheFilePath($ip)
    {
        return self::cacheDir() . '/' . hash('sha256', $ip) . '.json';
    }

    /**
     * @param string $ip
     * @return string|false|null
     */
    private static function getFile($ip)
    {
        $path = self::cacheFilePath($ip);
        if (!is_file($path)) {
            return false;
        }
        $raw = @file_get_contents($path);
        if ($raw === false || $raw === '') {
            return false;
        }
        $data = json_decode($raw, true);
        if (!is_array($data) || empty($data['expires_at']) || empty($data['value'])) {
            @unlink($path);
            return false;
        }
        if ((int) $data['expires_at'] < time()) {
            @unlink($path);
            return false;
        }
        return self::normalizeCachedValue($data['value'] === self::NONE_SENTINEL ? null : (string) $data['value']);
    }

    /**
     * @param string|null $value
     * @return string|null
     */
    private static function normalizeCachedValue($value)
    {
        if ($value === null || $value === '') {
            return null;
        }
        $normalized = CountryCodeNormalizer::toAlpha2($value);
        return $normalized !== null ? $normalized : strtoupper((string) $value);
    }

    /**
     * @param string $ip
     * @param string $value
     * @param int    $ttl
     */
    private static function setFile($ip, $value, $ttl)
    {
        $payload = json_encode([
            'value'      => $value,
            'expires_at' => time() + $ttl,
        ], JSON_UNESCAPED_UNICODE);
        if ($payload === false) {
            return;
        }
        @file_put_contents(self::cacheFilePath($ip), $payload, LOCK_EX);
    }

    public static function logError($message)
    {
        $handle = @fopen(dirname(__DIR__, 2) . '/err.txt', 'a+');
        if ($handle) {
            fwrite($handle, date('Y-m-d H:i:s') . ' | GeoIpCountryCache | ' . $message . "\n");
            fclose($handle);
        }
    }
}
