using System.Collections.Concurrent;

namespace Eis.Infrastructure.Authentication;

internal sealed class MemoryAuthenticationStateStore(AuthenticationOptions options) : IAuthenticationStateStore
{
    private readonly ConcurrentDictionary<string, ExpiringSession> _sessions = new(StringComparer.Ordinal);
    private readonly ConcurrentDictionary<string, ExpiringChallenge> _challenges = new(StringComparer.Ordinal);
    private readonly ConcurrentDictionary<string, ExpiringSetup> _setups = new(StringComparer.Ordinal);

    public string Backend => "memory";

    public int? Database => null;

    public int SessionTtlSeconds => options.SessionTtlSeconds;

    public Task CreateSessionAsync(string token, string userId)
    {
        _sessions[token] = new ExpiringSession(userId, ExpiresIn(options.SessionTtlSeconds));
        return Task.CompletedTask;
    }

    public Task<string?> GetSessionUserIdAsync(string token)
    {
        if (!_sessions.TryGetValue(token, out var item) || item.ExpiresAt <= DateTimeOffset.UtcNow)
        {
            _sessions.TryRemove(token, out _);
            return Task.FromResult<string?>(null);
        }

        return Task.FromResult<string?>(item.UserId);
    }

    public Task DeleteSessionAsync(string token)
    {
        _sessions.TryRemove(token, out _);
        return Task.CompletedTask;
    }

    public Task<int> DeleteUserSessionsAsync(string userId)
    {
        var deleted = 0;
        foreach (var item in _sessions)
        {
            if (item.Value.UserId == userId && _sessions.TryRemove(item.Key, out _))
            {
                deleted++;
            }
        }

        return Task.FromResult(deleted);
    }

    public async Task<int> DeleteUsersSessionsAsync(IEnumerable<string> userIds)
    {
        var deleted = 0;
        foreach (var userId in userIds.ToHashSet(StringComparer.Ordinal))
        {
            deleted += await DeleteUserSessionsAsync(userId);
        }

        return deleted;
    }

    public Task CreateLoginChallengeAsync(string key, string userId)
    {
        _challenges[key] = new ExpiringChallenge(userId, 0, ExpiresIn(options.LoginChallengeTtlSeconds));
        return Task.CompletedTask;
    }

    public Task<LoginChallenge?> GetLoginChallengeAsync(string key)
    {
        if (!TryGetLive(_challenges, key, out var item))
        {
            return Task.FromResult<LoginChallenge?>(null);
        }

        return Task.FromResult<LoginChallenge?>(new LoginChallenge(item.UserId, item.Attempts));
    }

    public Task<LoginChallenge?> RecordLoginChallengeFailureAsync(string key, int maximumAttempts)
    {
        while (TryGetLive(_challenges, key, out var item))
        {
            var updated = item with { Attempts = item.Attempts + 1 };
            if (!_challenges.TryUpdate(key, updated, item))
            {
                continue;
            }

            if (updated.Attempts >= maximumAttempts)
            {
                _challenges.TryRemove(key, out _);
            }

            return Task.FromResult<LoginChallenge?>(new LoginChallenge(updated.UserId, updated.Attempts));
        }

        return Task.FromResult<LoginChallenge?>(null);
    }

    public Task DeleteLoginChallengeAsync(string key)
    {
        _challenges.TryRemove(key, out _);
        return Task.CompletedTask;
    }

    public Task CreateTotpSetupAsync(string token, string userId, string secret)
    {
        _setups[token] = new ExpiringSetup(userId, secret, ExpiresIn(options.TotpSetupTtlSeconds));
        return Task.CompletedTask;
    }

    public Task<TotpSetup?> GetTotpSetupAsync(string token)
    {
        if (!TryGetLive(_setups, token, out var item))
        {
            return Task.FromResult<TotpSetup?>(null);
        }

        return Task.FromResult<TotpSetup?>(new TotpSetup(item.UserId, item.Secret));
    }

    public Task DeleteTotpSetupAsync(string token)
    {
        _setups.TryRemove(token, out _);
        return Task.CompletedTask;
    }

    private static DateTimeOffset ExpiresIn(int seconds) => DateTimeOffset.UtcNow.AddSeconds(seconds);

    private static bool TryGetLive<T>(ConcurrentDictionary<string, T> items, string key, out T item)
        where T : IExpiring
    {
        if (items.TryGetValue(key, out item!) && item.ExpiresAt > DateTimeOffset.UtcNow)
        {
            return true;
        }

        items.TryRemove(key, out _);
        item = default!;
        return false;
    }

    private interface IExpiring
    {
        DateTimeOffset ExpiresAt { get; }
    }

    private sealed record ExpiringSession(string UserId, DateTimeOffset ExpiresAt) : IExpiring;

    private sealed record ExpiringChallenge(string UserId, int Attempts, DateTimeOffset ExpiresAt) : IExpiring;

    private sealed record ExpiringSetup(string UserId, string Secret, DateTimeOffset ExpiresAt) : IExpiring;
}
