using StackExchange.Redis;

namespace Eis.Infrastructure.Authentication;

internal sealed class RedisAuthenticationStateStore : IAuthenticationStateStore, IDisposable
{
    private const string RecordFailureScript = """
        if redis.call('EXISTS', KEYS[1]) == 0 then
          return -1
        end
        local attempts = redis.call('HINCRBY', KEYS[1], 'attempts', 1)
        if attempts >= tonumber(ARGV[1]) then
          redis.call('DEL', KEYS[1])
        end
        return attempts
        """;

    private readonly AuthenticationOptions _options;
    private readonly ConnectionMultiplexer _connection;
    private readonly IDatabase _database;

    public RedisAuthenticationStateStore(AuthenticationOptions options)
    {
        _options = options;
        var configuration = BuildConfiguration(options.SessionRedisUrl!);
        configuration.DefaultDatabase = options.SessionRedisDatabase;
        configuration.ConnectTimeout = options.RedisConnectTimeoutMilliseconds;
        configuration.AbortOnConnectFail = true;
        try
        {
            _connection = ConnectionMultiplexer.Connect(configuration);
            _database = _connection.GetDatabase(options.SessionRedisDatabase);
        }
        catch (RedisException error)
        {
            throw new InvalidOperationException($"Redis 认证状态存储连接失败：{error.Message}", error);
        }
    }

    public string Backend => "redis";

    public int? Database => _options.SessionRedisDatabase;

    public int SessionTtlSeconds => _options.SessionTtlSeconds;

    public async Task CreateSessionAsync(string token, string userId)
    {
        var transaction = _database.CreateTransaction();
        _ = transaction.StringSetAsync(SessionKey(token), userId, TimeSpan.FromSeconds(_options.SessionTtlSeconds));
        _ = transaction.SetAddAsync(UserSessionsKey(userId), token);
        _ = transaction.KeyExpireAsync(UserSessionsKey(userId), TimeSpan.FromSeconds(_options.SessionTtlSeconds));
        if (!await transaction.ExecuteAsync())
        {
            throw new InvalidOperationException("Redis 会话写入事务未能执行");
        }
    }

    public async Task<string?> GetSessionUserIdAsync(string token)
    {
        var value = await _database.StringGetAsync(SessionKey(token));
        return value.HasValue ? value.ToString() : null;
    }

    public async Task DeleteSessionAsync(string token)
    {
        var key = SessionKey(token);
        var userId = await _database.StringGetAsync(key);
        var transaction = _database.CreateTransaction();
        _ = transaction.KeyDeleteAsync(key);
        if (userId.HasValue)
        {
            _ = transaction.SetRemoveAsync(UserSessionsKey(userId.ToString()), token);
        }

        await transaction.ExecuteAsync();
    }

    public async Task<int> DeleteUserSessionsAsync(string userId)
    {
        var indexKey = UserSessionsKey(userId);
        var tokens = await _database.SetMembersAsync(indexKey);
        if (tokens.Length == 0)
        {
            await _database.KeyDeleteAsync(indexKey);
            return 0;
        }

        var transaction = _database.CreateTransaction();
        foreach (var token in tokens)
        {
            _ = transaction.KeyDeleteAsync(SessionKey(token.ToString()));
        }
        _ = transaction.KeyDeleteAsync(indexKey);
        if (!await transaction.ExecuteAsync())
        {
            throw new InvalidOperationException("Redis 用户会话失效事务未能执行");
        }

        return tokens.Length;
    }

    public async Task<int> DeleteUsersSessionsAsync(IEnumerable<string> userIds)
    {
        var tasks = userIds.ToHashSet(StringComparer.Ordinal).Select(DeleteUserSessionsAsync);
        var counts = await Task.WhenAll(tasks);
        return counts.Sum();
    }

    public async Task CreateLoginChallengeAsync(string key, string userId)
    {
        var redisKey = LoginChallengeKey(key);
        var transaction = _database.CreateTransaction();
        _ = transaction.HashSetAsync(redisKey,
        [
            new HashEntry("userId", userId),
            new HashEntry("attempts", "0")
        ]);
        _ = transaction.KeyExpireAsync(redisKey, TimeSpan.FromSeconds(_options.LoginChallengeTtlSeconds));
        if (!await transaction.ExecuteAsync())
        {
            throw new InvalidOperationException("Redis 登录挑战写入事务未能执行");
        }
    }

    public async Task<LoginChallenge?> GetLoginChallengeAsync(string key)
    {
        var values = await _database.HashGetAsync(LoginChallengeKey(key), ["userId", "attempts"]);
        if (!values[0].HasValue)
        {
            return null;
        }

        return new LoginChallenge(values[0].ToString(), ParseAttempts(values[1]));
    }

    public async Task<LoginChallenge?> RecordLoginChallengeFailureAsync(string key, int maximumAttempts)
    {
        var challenge = await GetLoginChallengeAsync(key);
        if (challenge is null)
        {
            return null;
        }

        var result = await _database.ScriptEvaluateAsync(
            RecordFailureScript,
            [LoginChallengeKey(key)],
            [maximumAttempts]);
        var attempts = (int)(long)result;
        return attempts < 0 ? null : new LoginChallenge(challenge.UserId, attempts);
    }

    public Task DeleteLoginChallengeAsync(string key) => _database.KeyDeleteAsync(LoginChallengeKey(key));

    public async Task CreateTotpSetupAsync(string token, string userId, string secret)
    {
        var key = TotpSetupKey(token);
        var transaction = _database.CreateTransaction();
        _ = transaction.HashSetAsync(key,
        [
            new HashEntry("userId", userId),
            new HashEntry("secret", secret)
        ]);
        _ = transaction.KeyExpireAsync(key, TimeSpan.FromSeconds(_options.TotpSetupTtlSeconds));
        if (!await transaction.ExecuteAsync())
        {
            throw new InvalidOperationException("Redis TOTP 绑定状态写入事务未能执行");
        }
    }

    public async Task<TotpSetup?> GetTotpSetupAsync(string token)
    {
        var values = await _database.HashGetAsync(TotpSetupKey(token), ["userId", "secret"]);
        return values[0].HasValue && values[1].HasValue
            ? new TotpSetup(values[0].ToString(), values[1].ToString())
            : null;
    }

    public Task DeleteTotpSetupAsync(string token) => _database.KeyDeleteAsync(TotpSetupKey(token));

    public void Dispose() => _connection.Dispose();

    private RedisKey SessionKey(string token) => $"{_options.RedisPrefix}:session:{token}";

    private RedisKey UserSessionsKey(string userId) => $"{_options.RedisPrefix}:user-sessions:{userId}";

    private RedisKey LoginChallengeKey(string key) => $"{_options.RedisPrefix}:login-challenge:{key}";

    private RedisKey TotpSetupKey(string token) => $"{_options.RedisPrefix}:totp-setup:{token}";

    private static int ParseAttempts(RedisValue value) => int.TryParse(value.ToString(), out var attempts) ? attempts : 0;

    internal static ConfigurationOptions BuildConfiguration(string value)
    {
        if (!Uri.TryCreate(value, UriKind.Absolute, out var uri) ||
            uri.Scheme is not ("redis" or "rediss"))
        {
            throw new InvalidOperationException("REDIS_SESSION_URL/REDIS_URL 必须是有效的 redis:// 或 rediss:// 地址");
        }

        var configuration = new ConfigurationOptions
        {
            Ssl = uri.Scheme == "rediss",
            SslHost = uri.Scheme == "rediss" ? uri.Host : null
        };
        configuration.EndPoints.Add(uri.Host, uri.IsDefaultPort ? 6379 : uri.Port);
        if (!string.IsNullOrEmpty(uri.UserInfo))
        {
            var credentials = uri.UserInfo.Split(':', 2);
            if (credentials.Length == 2)
            {
                configuration.User = Uri.UnescapeDataString(credentials[0]);
                configuration.Password = Uri.UnescapeDataString(credentials[1]);
            }
            else
            {
                configuration.Password = Uri.UnescapeDataString(credentials[0]);
            }
        }

        return configuration;
    }
}
