using System.Collections.Concurrent;
using System.Security.Cryptography;

namespace Ikar.Server.Infrastructure.Auth;

public sealed class EmailAuthChallengeStore
{
    private static readonly TimeSpan ChallengeLifetime = TimeSpan.FromMinutes(10);
    private static readonly TimeSpan MinimumRequestInterval = TimeSpan.FromSeconds(60);
    private static readonly TimeSpan RequestWindow = TimeSpan.FromMinutes(15);
    private const int MaxRequestsPerWindow = 5;
    private const int MaxVerificationAttempts = 5;
    private readonly ConcurrentDictionary<string, EmailAuthChallenge> _challenges = new(StringComparer.Ordinal);
    private readonly ConcurrentDictionary<string, RequestBucket> _requestBuckets = new(StringComparer.Ordinal);

    public EmailAuthChallenge Create(string normalizedEmail, string normalizedPhoneNumber, bool isRegistered)
    {
        CleanupExpired();
        EnforceRequestLimit($"{normalizedEmail}|{normalizedPhoneNumber}");

        var challenge = new EmailAuthChallenge(
            Guid.NewGuid().ToString("N"),
            normalizedEmail,
            normalizedPhoneNumber,
            RandomNumberGenerator.GetInt32(0, 1_000_000).ToString("D6"),
            DateTimeOffset.UtcNow.Add(ChallengeLifetime),
            isRegistered);

        _challenges[challenge.Id] = challenge;
        return challenge;
    }

    public EmailAuthChallenge? Consume(
        string challengeId,
        string normalizedEmail,
        string normalizedPhoneNumber,
        string code)
    {
        CleanupExpired();

        if (!_challenges.TryGetValue(challengeId, out var challenge))
        {
            return null;
        }

        if (!string.Equals(challenge.NormalizedEmail, normalizedEmail, StringComparison.Ordinal) ||
            !string.Equals(challenge.NormalizedPhoneNumber, normalizedPhoneNumber, StringComparison.Ordinal) ||
            challenge.ExpiresAt <= DateTimeOffset.UtcNow)
        {
            return null;
        }

        if (!string.Equals(challenge.Code, code, StringComparison.Ordinal))
        {
            if (challenge.RegisterFailedAttempt() >= MaxVerificationAttempts)
            {
                _challenges.TryRemove(challenge.Id, out _);
            }

            return null;
        }

        _challenges.TryRemove(challenge.Id, out _);
        return challenge;
    }

    public void Remove(string challengeId)
    {
        _challenges.TryRemove(challengeId, out _);
    }

    private void CleanupExpired()
    {
        var now = DateTimeOffset.UtcNow;
        foreach (var pair in _challenges)
        {
            if (pair.Value.ExpiresAt <= now)
            {
                _challenges.TryRemove(pair.Key, out _);
            }
        }

        foreach (var pair in _requestBuckets)
        {
            if (now - pair.Value.WindowStartedAt >= RequestWindow)
            {
                _requestBuckets.TryRemove(pair.Key, out _);
            }
        }
    }

    private void EnforceRequestLimit(string key)
    {
        var now = DateTimeOffset.UtcNow;
        var bucket = _requestBuckets.GetOrAdd(key, _ => new RequestBucket(now));
        lock (bucket)
        {
            if (now - bucket.WindowStartedAt >= RequestWindow)
            {
                bucket.WindowStartedAt = now;
                bucket.LastRequestedAt = DateTimeOffset.MinValue;
                bucket.Count = 0;
            }

            var sinceLastRequest = now - bucket.LastRequestedAt;
            if (bucket.LastRequestedAt != DateTimeOffset.MinValue && sinceLastRequest < MinimumRequestInterval)
            {
                throw new AuthChallengeRateLimitException(MinimumRequestInterval - sinceLastRequest);
            }

            if (bucket.Count >= MaxRequestsPerWindow)
            {
                throw new AuthChallengeRateLimitException(RequestWindow - (now - bucket.WindowStartedAt));
            }

            bucket.LastRequestedAt = now;
            bucket.Count++;
        }
    }

    public sealed class EmailAuthChallenge(
        string id,
        string normalizedEmail,
        string normalizedPhoneNumber,
        string code,
        DateTimeOffset expiresAt,
        bool isRegistered)
    {
        private int _failedAttempts;

        public string Id { get; } = id;
        public string NormalizedEmail { get; } = normalizedEmail;
        public string NormalizedPhoneNumber { get; } = normalizedPhoneNumber;
        public string Code { get; } = code;
        public DateTimeOffset ExpiresAt { get; } = expiresAt;
        public bool IsRegistered { get; } = isRegistered;

        public int RegisterFailedAttempt() => Interlocked.Increment(ref _failedAttempts);
    }

    private sealed class RequestBucket(DateTimeOffset windowStartedAt)
    {
        public DateTimeOffset WindowStartedAt { get; set; } = windowStartedAt;
        public DateTimeOffset LastRequestedAt { get; set; } = DateTimeOffset.MinValue;
        public int Count { get; set; }
    }
}

public sealed class AuthChallengeRateLimitException(TimeSpan retryAfter) : Exception("Authentication challenge rate limit exceeded.")
{
    public TimeSpan RetryAfter { get; } = retryAfter;
}
