using StackExchange.Redis;
using System.Collections.Concurrent;
using adas_core.Application.Services.Interfaces;

namespace adas_core.Application.Services.Caching
{
    /// <summary>
    /// Lock distribuido en Redis. Usa token por adquisición (owner)
    /// y liberación segura con script Lua: borra la key solo si el valor coincide.
    /// </summary>
    public class RedisLockProvider(Func<IDatabase?> getDatabase) : ILockProvider
    {
        private readonly string _prefix = "lock:";
        private readonly TimeSpan _ttl = TimeSpan.FromSeconds(5);
        private readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(50);

        private static readonly string LuaReleaseScript = @"
        if redis.call('get', KEYS[1]) == ARGV[1] then
            return redis.call('del', KEYS[1])
        else
            return 0
        end";

        private readonly ConcurrentDictionary<string, string> _tokens =
            new(StringComparer.Ordinal);

        /// <summary>
        /// Attempts to acquire a distributed lock for the specified key using Redis, retrying until the timeout expires.
        /// Returns <c>false</c> if the Redis database is unavailable or if the lock cannot be acquired within the given timeout.
        /// </summary>
        /// <param name="key">The identifier of the resource to lock.</param>
        /// <param name="timeout">The maximum duration to keep retrying before giving up.</param>
        /// <returns><c>true</c> if the lock was successfully acquired; otherwise, <c>false</c>.</returns>
        public async Task<bool> AcquireAsync(string key, TimeSpan timeout)
                {
                    var redis = getDatabase();
                    if (redis is null) return false;
        
                    var redisKey = (RedisKey)(_prefix + key);
                    var token = Guid.NewGuid().ToString("N");
                    var end = DateTime.UtcNow.Add(timeout);
        
                    while (DateTime.UtcNow < end)
                    {
                        if (await redis.StringSetAsync(redisKey, token, _ttl, When.NotExists))
                        {
                            _tokens[key] = token;
                            return true;
                        }
        
                        await Task.Delay(_retryDelay);
                    }
        
                    return false;
                }

        /// <summary>
        /// Asynchronously releases the token associated with the specified key by removing it from the in-memory token store and executing a Lua release script against Redis. If the key is not found in the local store, or the Redis database is unavailable, the method returns without performing any further action.
        /// </summary>
        /// <param name="key">The identifier of the token to release.</param>
        public async Task ReleaseAsync(string key)
                {
                    if (!_tokens.TryRemove(key, out var token))
                        return;
        
                    var redis = getDatabase();
                    if (redis is null) return;
        
                    var redisKey = (RedisKey)(_prefix + key);
        
                    await redis.ScriptEvaluateAsync(
                        LuaReleaseScript,
                        [redisKey],
                        [token]
                    );
                }
    }
}