using Application.Abstractions.Cache;
using StackExchange.Redis;
using System.Text.Json;

namespace Infrastructure.Cache;

public sealed class RedisCacheService(IConnectionMultiplexer redis) : ICacheService
{
    private readonly IDatabase _db = redis.GetDatabase();

    private static readonly JsonSerializerOptions _jsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = false
    };

    public async Task<T?> GetAsync<T>(string key, CancellationToken ct = default)
    {
        var value = await _db.StringGetAsync(key);
        if (value.IsNullOrEmpty)
        {
            return default;
        }

        return JsonSerializer.Deserialize<T>((string)value!, _jsonOptions);
    }

    public async Task SetAsync<T>(string key, T value, CancellationToken ct = default)
    {
        await _db.StringSetAsync(key, JsonSerializer.Serialize(value, _jsonOptions));
    }

    public async Task SetAsync<T>(string key, T value, TimeSpan expiry, CancellationToken ct = default)
    {
        await _db.StringSetAsync(key, JsonSerializer.Serialize(value, _jsonOptions), expiry);
    }

    public async Task RemoveAsync(string key, CancellationToken ct = default)
    {
        await _db.KeyDeleteAsync(key);
    }

    public async Task RemoveByPrefixAsync(string prefix, CancellationToken ct = default)
    {
        // SCAN 기반 비블로킹 스캔. KEYS 명령은 운영 Redis 에서 단일 스레드 블로킹이라 절대 금지
        var server = redis.GetServer(_db.Multiplexer.GetEndPoints().First());
        var batch = new List<RedisKey>(256);

        await foreach (var key in server.KeysAsync(database: _db.Database, pattern: $"{prefix}*", pageSize: 1000).WithCancellation(ct))
        {
            batch.Add(key);
            if (batch.Count >= 500)
            {
                await _db.KeyDeleteAsync(batch.ToArray());
                batch.Clear();
            }
        }

        if (batch.Count > 0)
        {
            await _db.KeyDeleteAsync(batch.ToArray());
        }
    }

    public async Task<bool> TryLockAsync(string key, TimeSpan expiry, CancellationToken ct = default)
    {
        return await _db.StringSetAsync(key, "1", expiry, When.NotExists);
    }
}