using Microsoft.Extensions.Logging;
using StackExchange.Redis;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Tiku.Application.Security;

namespace Tiku.Infrastructure.Security;

internal sealed class RedisSecurityStore(
    IConnectionMultiplexer connection,
    string environmentName,
    ILogger<RedisSecurityStore> logger) : IRedisSecurityStore
{
    private static readonly Meter Meter = new("Tiku.Security.Redis", "1.0.0");
    private static readonly Counter<long> OperationCounter = Meter.CreateCounter<long>("tiku.redis.security.operations");
    private static readonly Counter<long> RejectionCounter = Meter.CreateCounter<long>("tiku.redis.rate_limit.rejections");
    private static readonly Counter<long> ErrorCounter = Meter.CreateCounter<long>("tiku.redis.security.errors");
    private static readonly Histogram<double> ScriptDuration = Meter.CreateHistogram<double>(
        "tiku.redis.lua.duration", "ms");
    private const string ConsumeScript = """
        local now = redis.call('TIME')
        local nowMs = now[1] * 1000 + math.floor(now[2] / 1000)
        local retryAfter = 0
        for i = 1, #KEYS do
            local current = tonumber(redis.call('GET', KEYS[i]) or '0')
            local limit = tonumber(ARGV[(i - 1) * 2 + 1])
            if current >= limit then
                local ttl = redis.call('PTTL', KEYS[i])
                if ttl > retryAfter then retryAfter = ttl end
            end
        end
        if retryAfter > 0 then return {0, retryAfter} end
        for i = 1, #KEYS do
            local window = tonumber(ARGV[(i - 1) * 2 + 2])
            local value = redis.call('INCR', KEYS[i])
            if value == 1 then redis.call('PEXPIRE', KEYS[i], window) end
        end
        return {1, 0}
        """;

    private readonly string prefix = $"tiku:{Normalize(environmentName)}";

    public bool IsConfigured => true;

    public async Task<DistributedRateLimitResult> ConsumeAsync(
        IReadOnlyCollection<DistributedRateLimitBucket> buckets,
        CancellationToken cancellationToken = default)
    {
        cancellationToken.ThrowIfCancellationRequested();
        if (buckets.Count == 0)
        {
            return new DistributedRateLimitResult(true);
        }

        try
        {
            var started = Stopwatch.GetTimestamp();
            var keys = buckets.Select(bucket => (RedisKey)$"{prefix}:rl:{bucket.Key}").ToArray();
            var values = buckets
                .SelectMany(bucket => new RedisValue[] { bucket.PermitLimit, (long)bucket.Window.TotalMilliseconds })
                .ToArray();
            var result = (RedisResult[])(await connection.GetDatabase()
                .ScriptEvaluateAsync(ConsumeScript, keys, values).WaitAsync(cancellationToken))!;
            var allowed = (long)result[0] == 1;
            var retryMs = (long)result[1];
            OperationCounter.Add(1, new KeyValuePair<string, object?>("operation", "rate_limit"));
            ScriptDuration.Record(Stopwatch.GetElapsedTime(started).TotalMilliseconds);
            if (!allowed)
            {
                RejectionCounter.Add(1);
            }
            return new DistributedRateLimitResult(
                allowed,
                retryMs > 0 ? TimeSpan.FromMilliseconds(retryMs) : null);
        }
        catch (Exception exception) when (exception is RedisException or TimeoutException)
        {
            ErrorCounter.Add(1, new KeyValuePair<string, object?>("operation", "rate_limit"));
            logger.LogError(exception, "Redis security operation failed closed.");
            throw new RedisSecurityUnavailableException(exception);
        }
    }

    public async Task<bool> PingAsync(CancellationToken cancellationToken = default)
    {
        try
        {
            await connection.GetDatabase().PingAsync().WaitAsync(cancellationToken);
            OperationCounter.Add(1, new KeyValuePair<string, object?>("operation", "ping"));
            return true;
        }
        catch (Exception exception) when (exception is RedisException or TimeoutException)
        {
            ErrorCounter.Add(1, new KeyValuePair<string, object?>("operation", "ping"));
            return false;
        }
    }

    private static string Normalize(string value) =>
        string.Concat(value.Trim().ToLowerInvariant().Select(character =>
            char.IsLetterOrDigit(character) || character is '-' or '_' ? character : '-'));
}

public sealed class NullRedisSecurityStore : IRedisSecurityStore
{
    public bool IsConfigured => false;

    public Task<DistributedRateLimitResult> ConsumeAsync(
        IReadOnlyCollection<DistributedRateLimitBucket> buckets,
        CancellationToken cancellationToken = default) =>
        Task.FromResult(new DistributedRateLimitResult(true));

    public Task<bool> PingAsync(CancellationToken cancellationToken = default) => Task.FromResult(false);
}

public sealed class RedisSecurityUnavailableException(Exception innerException)
    : Exception("Redis security services are unavailable.", innerException);
