<?php

declare(strict_types=1);

namespace app\common\service;

use support\Redis;
use Throwable;

/**
 * 热点实体互斥锁（Redis SET NX EX），按资源串行化写入，避免多管理员/多进程同时改同一缓存键对应的数据。
 */
final class GameHotDataLock
{
    public const TYPE_USER = 'user';

    public const TYPE_GAME_CONFIG = 'gc';

    public const TYPE_GAME_RECORD = 'gr';

    private const KEY_PREFIX = 'dfw:v1:lock:mut:';

    /**
     * @return array{acquired: bool, token: ?string, redis_lock: bool}
     */
    public static function tryAcquire(string $type, string $resourceKey): array
    {
        if ($resourceKey === '') {
            return ['acquired' => false, 'token' => null, 'redis_lock' => false];
        }
        $lockKey = self::lockKey($type, $resourceKey);
        $ttl = self::lockTtl();
        $token = bin2hex(random_bytes(16));
        try {
            $client = Redis::connection()->client();
            if (!is_object($client) || !method_exists($client, 'set')) {
                return ['acquired' => true, 'token' => null, 'redis_lock' => false];
            }
            $ok = $client->set($lockKey, $token, ['nx', 'ex' => $ttl]);
            if ($ok === true) {
                return ['acquired' => true, 'token' => $token, 'redis_lock' => true];
            }
        } catch (Throwable) {
            return ['acquired' => true, 'token' => null, 'redis_lock' => false];
        }

        return ['acquired' => false, 'token' => null, 'redis_lock' => true];
    }

    /**
     * 带短等待的重试（毫秒），用于对局写入与 ensureAiLocked 等可能交叉的路径。
     *
     * @return array{acquired: bool, token: ?string, redis_lock: bool}
     */
    public static function tryAcquireWithWait(string $type, string $resourceKey, int $maxWaitMs = 800): array
    {
        $deadline = (int) (microtime(true) * 1000) + max(0, $maxWaitMs);
        while (true) {
            $r = self::tryAcquire($type, $resourceKey);
            if ($r['acquired']) {
                return $r;
            }
            $now = (int) (microtime(true) * 1000);
            if ($now >= $deadline) {
                return $r;
            }
            usleep(25_000);
        }
    }

    public static function release(string $type, string $resourceKey, ?string $token, bool $redisLock): void
    {
        if ($resourceKey === '' || !$redisLock || $token === null || $token === '') {
            return;
        }
        $key = self::lockKey($type, $resourceKey);
        $script = <<<'LUA'
if redis.call('get', KEYS[1]) == ARGV[1] then
  return redis.call('del', KEYS[1])
end
return 0
LUA;
        try {
            $client = Redis::connection()->client();
            if (is_object($client) && method_exists($client, 'eval')) {
                $client->eval($script, [$key, $token], 1);
            }
        } catch (Throwable) {
        }
    }

    /**
     * @template T
     * @param callable(): T $fn
     * @return T|null
     */
    public static function runExclusive(string $type, string $resourceKey, callable $fn): mixed
    {
        $lock = self::tryAcquire($type, $resourceKey);
        if (!$lock['acquired']) {
            return null;
        }
        try {
            return $fn();
        } finally {
            self::release($type, $resourceKey, $lock['token'], $lock['redis_lock']);
        }
    }

    public static function safeResourceKeyForConfig(string $configKey): string
    {
        return rtrim(strtr(base64_encode($configKey), '+/', '-_'), '=');
    }

    public static function lockKeyFromConfigKey(string $configKey): string
    {
        return self::lockKey(self::TYPE_GAME_CONFIG, self::safeResourceKeyForConfig($configKey));
    }

    private static function lockKey(string $type, string $resourceKey): string
    {
        return self::KEY_PREFIX . $type . ':' . $resourceKey;
    }

    private static function lockTtl(): int
    {
        $v = config('game_hot_cache.admin_user_mutation_lock_ttl', 30);
        $n = filter_var($v, FILTER_VALIDATE_INT);

        return ($n === false || $n < 5) ? 30 : $n;
    }
}
