<?php

declare(strict_types=1);

namespace app\common\service;

use app\common\model\User;
use support\Redis;
use support\think\Db;
use Throwable;

/**
 * 热点数据 Redis 缓存：game_config、game_record、user 全行 JSON。
 * Redis 不可用时自动回退为直连数据库。
 */
final class GameHotDataRedis
{
    private const KEY_GC = 'dfw:v1:gc:';

    private const KEY_GR_ID = 'dfw:v1:gr:id:';

    private const KEY_GR_ACTIVE = 'dfw:v1:gr:active';

    private const KEY_GR_LATEST = 'dfw:v1:gr:latest';

    private const KEY_USER = 'dfw:v1:user:';

    /**
     * 进程内实例化缓存（避免同一请求/进程内重复打 Redis/DB）。
     * - key => null：表示已确认不存在（负缓存）
     * - key => array：表示已加载行数据
     *
     * @var array<string, array<string, mixed>|null>
     */
    private static array $gcLocal = [];

    public static function enabled(): bool
    {
        return config('game_hot_cache.enabled', true) === true;
    }

    /**
     * @return array<string, mixed>|null
     */
    public static function gameConfigRow(string $configKey): ?array
    {
        if ($configKey === '') {
            return null;
        }
        // game_config 为全局配置，多进程/多 worker 间必须强一致；
        // 因此不使用进程内本地缓存（gcLocal），避免某个进程读到旧值导致前端回弹/行为冲突。
        if (self::enabled()) {
            $cached = self::redisGet(self::KEY_GC . $configKey);
            if ($cached !== null && $cached !== '') {
                $decoded = json_decode($cached, true);
                if (is_array($decoded)) {
                    return $decoded;
                }
            }
        }
        $row = Db::name('game_config')->where('config_key', $configKey)->find();
        if (!$row) {
            return null;
        }
        if (self::enabled()) {
            $ttl = self::intConfig('ttl_game_config', 86400);
            self::redisSetEx(self::KEY_GC . $configKey, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
        }
        return $row;
    }

    public static function gameConfigForget(string $configKey): void
    {
        if ($configKey === '') {
            return;
        }
        self::redisDel(self::KEY_GC . $configKey);
        unset(self::$gcLocal[$configKey]);
    }

    /**
     * 按库中当前行覆盖 game_config 缓存（无行则删缓存）
     */
    public static function gameConfigReplaceFromDb(string $configKey): void
    {
        if ($configKey === '') {
            return;
        }
        // 无论是否启用 Redis 热点缓存，都要刷新进程内缓存，避免同一 worker 读到旧值
        $row = Db::name('game_config')->where('config_key', $configKey)->find();
        if (!$row) {
            self::$gcLocal[$configKey] = null;
            if (self::enabled()) {
                self::redisDel(self::KEY_GC . $configKey);
            }
            return;
        }

        self::$gcLocal[$configKey] = $row;
        if (self::enabled()) {
            $ttl = self::intConfig('ttl_game_config', 86400);
            self::redisSetEx(self::KEY_GC . $configKey, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
        }
    }

    /**
     * 对局写入后：刷新指定 id 的行缓存，并删除「活跃局 / 最新局」聚合键以免脏读
     *
     * @param int|null $id 可为 null（仅清聚合键）
     */
    public static function gameRecordSyncCachesAfterDbWrite(?int $id): void
    {
        if (!self::enabled()) {
            return;
        }
        if ($id !== null && $id > 0) {
            $row = Db::name('game_record')->where('id', $id)->find();
            if ($row) {
                $ttl = self::intConfig('ttl_game_record', 60);
                self::redisSetEx(self::KEY_GR_ID . $id, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
            } else {
                self::redisDel(self::KEY_GR_ID . $id);
            }
        }
        self::redisDel(self::KEY_GR_ACTIVE, self::KEY_GR_LATEST);
    }

    /**
     * @return array<string, mixed>|null
     */
    public static function gameRecordById(int $id): ?array
    {
        if ($id <= 0) {
            return null;
        }
        $key = self::KEY_GR_ID . $id;
        if (self::enabled()) {
            $cached = self::redisGet($key);
            if ($cached !== null && $cached !== '') {
                $decoded = json_decode($cached, true);
                if (is_array($decoded)) {
                    return $decoded;
                }
            }
        }
        $row = Db::name('game_record')->where('id', $id)->find();
        if (!$row) {
            return null;
        }
        if (self::enabled()) {
            $ttl = self::intConfig('ttl_game_record', 60);
            self::redisSetEx($key, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
        }
        return $row;
    }

    /**
     * 当前未完全结束的对局（status ∈ 0,1,2,3 中 id 最大的一条）
     *
     * @return array<string, mixed>|null
     */
    public static function gameRecordActive(): ?array
    {
        if (self::enabled()) {
            $cached = self::redisGet(self::KEY_GR_ACTIVE);
            if ($cached !== null && $cached !== '') {
                $decoded = json_decode($cached, true);
                if (is_array($decoded)) {
                    return $decoded;
                }
            }
        }
        $row = Db::name('game_record')->whereIn('status', [0, 1, 2, 3])->order('id', 'desc')->find();
        if (!$row) {
            return null;
        }
        if (self::enabled()) {
            $ttl = self::intConfig('ttl_game_record', 60);
            self::redisSetEx(self::KEY_GR_ACTIVE, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
        }
        return $row;
    }

    /**
     * 最新一条对局（含已结束），与 GameRecord::order('id','desc')->find() 一致
     *
     * @return array<string, mixed>|null
     */
    public static function gameRecordLatest(): ?array
    {
        if (self::enabled()) {
            $cached = self::redisGet(self::KEY_GR_LATEST);
            if ($cached !== null && $cached !== '') {
                $decoded = json_decode($cached, true);
                if (is_array($decoded)) {
                    return $decoded;
                }
            }
        }
        $row = Db::name('game_record')->order('id', 'desc')->find();
        if (!$row) {
            return null;
        }
        if (self::enabled()) {
            $ttl = self::intConfig('ttl_game_record', 60);
            self::redisSetEx(self::KEY_GR_LATEST, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
        }
        return $row;
    }

    /**
     * 任意 game_record 变更后调用：删除活跃局、最新局及指定 id 缓存
     */
    public static function gameRecordForget(?int $id = null): void
    {
        $keys = [self::KEY_GR_ACTIVE, self::KEY_GR_LATEST];
        if ($id !== null && $id > 0) {
            $keys[] = self::KEY_GR_ID . $id;
        }
        self::redisDel(...$keys);
    }

    /**
     * @return array<string, mixed>|null
     */
    public static function userRow(int $userId): ?array
    {
        if ($userId <= 0) {
            return null;
        }
        $key = self::KEY_USER . $userId;
        if (self::enabled()) {
            $cached = self::redisGet($key);
            if ($cached !== null && $cached !== '') {
                $decoded = json_decode($cached, true);
                if (is_array($decoded)) {
                    return $decoded;
                }
            }
        }
        $row = Db::name('user')->where('id', $userId)->find();
        if (!$row) {
            return null;
        }
        if (self::enabled()) {
            $ttl = self::intConfig('ttl_user', 90);
            self::redisSetEx($key, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
        }
        return $row;
    }

    public static function userRemember(User $user): void
    {
        if (!self::enabled()) {
            return;
        }
        $id = $user->getAttr('id');
        $uid = filter_var($id, FILTER_VALIDATE_INT);
        if ($uid === false || $uid <= 0) {
            return;
        }
        $ttl = self::intConfig('ttl_user', 90);
        self::redisSetEx(self::KEY_USER . $uid, $ttl, json_encode($user->toArray(), JSON_UNESCAPED_UNICODE));
    }

    public static function userForget(int $userId): void
    {
        if ($userId <= 0) {
            return;
        }
        self::redisDel(self::KEY_USER . $userId);
    }

    /**
     * 从数据库读取最新 user 行并覆盖写入 Redis（与 DB 同事务后调用，保持缓存与库一致）
     */
    public static function userReplaceCacheFromDb(int $userId): void
    {
        if ($userId <= 0 || !self::enabled()) {
            return;
        }
        $row = Db::name('user')->where('id', $userId)->find();
        if (!$row) {
            self::userForget($userId);
            return;
        }
        $ttl = self::intConfig('ttl_user', 90);
        self::redisSetEx(self::KEY_USER . $userId, $ttl, json_encode($row, JSON_UNESCAPED_UNICODE));
    }

    /**
     * 尝试获取「后台修改该用户」互斥锁（Redis SET NX EX）。
     * 与热点缓存开关无关：只要 Redis 可用即加锁；连接失败时降级为仅依赖数据库乐观锁（WHERE coin=）。
     *
     * @return array{acquired: bool, token: ?string, redis_lock: bool}
     */
    public static function userAdminMutationLockTry(int $userId): array
    {
        if ($userId <= 0) {
            return ['acquired' => false, 'token' => null, 'redis_lock' => false];
        }

        return GameHotDataLock::tryAcquire(GameHotDataLock::TYPE_USER, (string) $userId);
    }

    /**
     * 释放 userAdminMutationLockTry 取得的锁（仅当 redis_lock 且 token 非空）
     */
    public static function userAdminMutationLockRelease(int $userId, ?string $token, bool $redisLock): void
    {
        if ($userId <= 0) {
            return;
        }
        GameHotDataLock::release(GameHotDataLock::TYPE_USER, (string) $userId, $token, $redisLock);
    }

    /**
     * 用缓存行构造已存在库的 User（供 Auth 等高频读）
     */
    public static function userModelFromCacheOrDb(int $userId): ?User
    {
        $row = self::userRow($userId);
        if ($row === null) {
            return null;
        }
        return (new User())->newInstance($row);
    }

    private static function intConfig(string $name, int $default): int
    {
        $v = config('game_hot_cache.' . $name, $default);
        if (is_int($v)) {
            return $v > 0 ? $v : $default;
        }
        if (is_numeric($v)) {
            $n = filter_var($v, FILTER_VALIDATE_INT);
            if ($n === false) {
                return $default;
            }

            return $n > 0 ? $n : $default;
        }

        return $default;
    }

    private static function redisGet(string $key): ?string
    {
        try {
            $v = Redis::get($key);
            if ($v === false || $v === null) {
                return null;
            }
            if (!is_string($v)) {
                return null;
            }
            return $v;
        } catch (Throwable) {
            return null;
        }
    }

    private static function redisSetEx(string $key, int $ttl, string $value): void
    {
        try {
            Redis::setEx($key, $ttl, $value);
        } catch (Throwable) {
        }
    }

    private static function redisDel(string ...$keys): void
    {
        if ($keys === []) {
            return;
        }
        try {
            Redis::del(...$keys);
        } catch (Throwable) {
        }
    }
}
