using ColaFlow.Modules.Identity.Application.Services;
using ColaFlow.Modules.Identity.Domain.Entities;
using ColaFlow.Modules.Identity.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace ColaFlow.Modules.Identity.Infrastructure.Services;

/// <summary>
/// Database-backed rate limiting service implementation.
/// Persists rate limit state in PostgreSQL to survive server restarts.
/// Prevents email bombing attacks even after application restart.
/// </summary>
public class DatabaseEmailRateLimiter(
    IdentityDbContext context,
    ILogger<DatabaseEmailRateLimiter> logger)
    : IRateLimitService
{
    public async Task<bool> IsAllowedAsync(
        string key,
        int maxAttempts,
        TimeSpan window,
        CancellationToken cancellationToken = default)
    {
        // Parse key format: "operation:email:tenantId"
        // Examples:
        // - "forgot-password:user@example.com:tenant-guid"
        // - "verification:user@example.com:tenant-guid"
        // - "invitation:user@example.com:tenant-guid"

        var parts = key.Split(':');
        if (parts.Length != 3)
        {
            logger.LogWarning("Invalid rate limit key format: {Key}. Expected format: 'operation:email:tenantId'", key);
            return true; // Fail open (allow request) if key format is invalid
        }

        var operationType = parts[0];
        var email = parts[1].ToLower();
        var tenantIdStr = parts[2];

        if (!Guid.TryParse(tenantIdStr, out var tenantId))
        {
            logger.LogWarning("Invalid tenant ID in rate limit key: {Key}", key);
            return true; // Fail open
        }

        // Find existing rate limit record
        var rateLimit = await context.EmailRateLimits
            .FirstOrDefaultAsync(
                r => r.Email == email &&
                     r.TenantId == tenantId &&
                     r.OperationType == operationType,
                cancellationToken);

        // No existing record - create new one and allow
        if (rateLimit == null)
        {
            var newRateLimit = EmailRateLimit.Create(email, tenantId, operationType);
            context.EmailRateLimits.Add(newRateLimit);

            try
            {
                await context.SaveChangesAsync(cancellationToken);
                logger.LogInformation(
                    "Rate limit record created for {Email} - {Operation} (Attempt 1/{MaxAttempts})",
                    email, operationType, maxAttempts);
            }
            catch (DbUpdateException ex)
            {
                // Handle race condition: another request created the record simultaneously
                logger.LogWarning(ex,
                    "Race condition detected while creating rate limit record for {Key}. Retrying...", key);

                // Re-fetch the record created by the concurrent request
                rateLimit = await context.EmailRateLimits
                    .FirstOrDefaultAsync(
                        r => r.Email == email &&
                             r.TenantId == tenantId &&
                             r.OperationType == operationType,
                        cancellationToken);

                if (rateLimit == null)
                {
                    logger.LogError("Failed to fetch rate limit record after race condition for {Key}", key);
                    return true; // Fail open
                }

                // Fall through to existing record logic below
            }

            if (rateLimit == null)
                return true; // Record was successfully created, allow the request
        }

        // Check if time window has expired
        if (rateLimit.IsWindowExpired(window))
        {
            // Window expired - reset counter and allow
            rateLimit.ResetAttempts();
            context.EmailRateLimits.Update(rateLimit);
            await context.SaveChangesAsync(cancellationToken);

            logger.LogInformation(
                "Rate limit window expired for {Email} - {Operation}. Counter reset (Attempt 1/{MaxAttempts})",
                email, operationType, maxAttempts);

            return true;
        }

        // Window still active - check attempt count
        if (rateLimit.AttemptsCount >= maxAttempts)
        {
            // Rate limit exceeded
            var remainingTime = window - (DateTime.UtcNow - rateLimit.LastSentAt);

            logger.LogWarning(
                "Rate limit EXCEEDED for {Email} - {Operation}: {Attempts}/{MaxAttempts} attempts. " +
                "Retry after {RemainingSeconds} seconds",
                email, operationType, rateLimit.AttemptsCount, maxAttempts,
                (int)remainingTime.TotalSeconds);

            return false;
        }

        // Still within limit - increment counter and allow
        rateLimit.RecordAttempt();
        context.EmailRateLimits.Update(rateLimit);
        await context.SaveChangesAsync(cancellationToken);

        logger.LogInformation(
            "Rate limit check passed for {Email} - {Operation} (Attempt {Attempts}/{MaxAttempts})",
            email, operationType, rateLimit.AttemptsCount, maxAttempts);

        return true;
    }

    /// <summary>
    /// Cleanup expired rate limit records (call this from a background job)
    /// </summary>
    public async Task CleanupExpiredRecordsAsync(TimeSpan retentionPeriod, CancellationToken cancellationToken = default)
    {
        var cutoffDate = DateTime.UtcNow - retentionPeriod;

        var expiredRecords = await context.EmailRateLimits
            .Where(r => r.LastSentAt < cutoffDate)
            .ToListAsync(cancellationToken);

        if (expiredRecords.Any())
        {
            context.EmailRateLimits.RemoveRange(expiredRecords);
            await context.SaveChangesAsync(cancellationToken);

            logger.LogInformation(
                "Cleaned up {Count} expired rate limit records older than {CutoffDate}",
                expiredRecords.Count, cutoffDate);
        }
    }
}
