using MongoDB.Driver; using MongoDB.Bson; using QRRapidoApp.Data; using QRRapidoApp.Models; using QRRapidoApp.Models.ViewModels; using System.Text.Json; namespace QRRapidoApp.Services { public class UserService : IUserService { private readonly MongoDbContext _context; private readonly IConfiguration _config; private readonly ILogger _logger; public UserService(MongoDbContext context, IConfiguration config, ILogger logger) { _context = context; _config = config; _logger = logger; } public async Task GetUserAsync(string userId) { try { if (_context.Users == null) return null; // Development mode without MongoDB User? userData = null; if (!String.IsNullOrEmpty(userId)) { userData = await _context.Users.Find(u => u.Id == userId).FirstOrDefaultAsync(); } return userData ?? new User(); } catch (Exception ex) { _logger.LogError(ex, $"Error getting user {userId}: {ex.Message}"); return null; } } public async Task GetUserByEmailAsync(string email) { try { if (_context.Users == null) return null; // Development mode without MongoDB return await _context.Users.Find(u => u.Email == email).FirstOrDefaultAsync(); } catch (Exception ex) { _logger.LogError(ex, $"Error getting user by email {email}: {ex.Message}"); return null; } } public async Task GetUserByProviderAsync(string provider, string providerId) { try { return await _context.Users .Find(u => u.Provider == provider && u.ProviderId == providerId) .FirstOrDefaultAsync(); } catch (Exception ex) { _logger.LogError(ex, $"Error getting user by provider {provider}:{providerId}: {ex.Message}"); return null; } } public async Task CreateUserAsync(string email, string name, string provider, string providerId) { var user = new User { Email = email, Name = name, Provider = provider, ProviderId = providerId, CreatedAt = DateTime.UtcNow, LastLoginAt = DateTime.UtcNow, PreferredLanguage = "pt-BR", DailyQRCount = 0, LastQRDate = DateTime.UtcNow.Date, TotalQRGenerated = 0 }; await _context.Users.InsertOneAsync(user); _logger.LogInformation($"Created new user: {email} via {provider}"); return user; } public async Task UpdateLastLoginAsync(string userId) { try { var update = Builders.Update .Set(u => u.LastLoginAt, DateTime.UtcNow); await _context.Users.UpdateOneAsync(u => u.Id == userId, update); } catch (Exception ex) { _logger.LogError(ex, $"Error updating last login for user {userId}: {ex.Message}"); } } public async Task UpdateUserAsync(User user) { try { var result = await _context.Users.ReplaceOneAsync(u => u.Id == user.Id, user); return result.ModifiedCount > 0; } catch (Exception ex) { _logger.LogError(ex, $"Error updating user {user.Id}: {ex.Message}"); return false; } } public async Task GetDailyQRCountAsync(string? userId) { if (string.IsNullOrEmpty(userId)) return 0; // Anonymous users tracked separately try { var user = await GetUserAsync(userId); if (user == null) return 0; // Reset count if it's a new day if (user.LastQRDate.Date < DateTime.UtcNow.Date) { user.DailyQRCount = 0; user.LastQRDate = DateTime.UtcNow.Date; await UpdateUserAsync(user); } return user.DailyQRCount; } catch (Exception ex) { _logger.LogError(ex, $"Error getting daily QR count for user {userId}: {ex.Message}"); return 0; } } public async Task IncrementDailyQRCountAsync(string userId) { try { var user = await GetUserAsync(userId); if (user == null) return 0; // Reset count if it's a new day if (user.LastQRDate.Date < DateTime.UtcNow.Date) { user.DailyQRCount = 1; user.LastQRDate = DateTime.UtcNow.Date; } else { user.DailyQRCount++; } user.TotalQRGenerated++; await UpdateUserAsync(user); // Premium and logged users have unlimited QR codes return int.MaxValue; } catch (Exception ex) { _logger.LogError(ex, $"Error incrementing daily QR count for user {userId}: {ex.Message}"); return 0; } } public async Task GetRemainingQRCountAsync(string userId) { try { var user = await GetUserAsync(userId); if (user == null) return 0; // Premium users have unlimited if (user.IsPremium) return int.MaxValue; // Logged users (non-premium) have unlimited return int.MaxValue; } catch (Exception ex) { _logger.LogError(ex, $"Error getting remaining QR count for user {userId}: {ex.Message}"); return 0; } } public async Task CanGenerateQRAsync(string? userId, bool isPremium) { // Premium users have unlimited QR codes if (isPremium) return true; // Logged users (non-premium) have unlimited QR codes if (!string.IsNullOrEmpty(userId)) return true; // Anonymous users have 3 QR codes per day var dailyCount = await GetDailyQRCountAsync(userId); var limit = 3; return dailyCount < limit; } public async Task SaveQRToHistoryAsync(string? userId, QRGenerationResult qrResult, int costInCredits = 0) { try { var qrHistory = new QRCodeHistory { Id = string.IsNullOrEmpty(qrResult.QRId) ? Guid.NewGuid().ToString() : qrResult.QRId, UserId = userId, Type = qrResult.RequestSettings?.Type ?? "unknown", Content = qrResult.RequestSettings?.Content ?? "", QRCodeBase64 = qrResult.QRCodeBase64, CustomizationSettings = JsonSerializer.Serialize(qrResult.RequestSettings), CreatedAt = DateTime.UtcNow, Language = qrResult.RequestSettings?.Language ?? "pt-BR", Size = qrResult.RequestSettings?.Size ?? 300, GenerationTimeMs = qrResult.GenerationTimeMs, FromCache = qrResult.FromCache, IsActive = true, LastAccessedAt = DateTime.UtcNow, TrackingId = qrResult.TrackingId, IsDynamic = !string.IsNullOrEmpty(qrResult.TrackingId), CostInCredits = costInCredits }; await _context.QRCodeHistory.InsertOneAsync(qrHistory); } catch (Exception ex) { _logger.LogError(ex, $"Error saving QR to history: {ex.Message}"); } } public async Task> GetUserQRHistoryAsync(string userId, int limit = 50) { try { return await _context.QRCodeHistory .Find(q => q.UserId == userId && q.IsActive) .SortByDescending(q => q.CreatedAt) .Limit(limit) .ToListAsync(); } catch (Exception ex) { _logger.LogError(ex, $"Error getting QR history for user {userId}: {ex.Message}"); return new List(); } } public async Task GetQRDataAsync(string qrId) { try { return await _context.QRCodeHistory .Find(q => q.Id == qrId && q.IsActive) .FirstOrDefaultAsync(); } catch (Exception ex) { _logger.LogError(ex, $"Error getting QR data {qrId}: {ex.Message}"); return null; } } public async Task DeleteQRFromHistoryAsync(string userId, string qrId) { try { // First verify that the QR code belongs to the user var qrCode = await _context.QRCodeHistory .Find(q => q.Id == qrId && q.UserId == userId && q.IsActive) .FirstOrDefaultAsync(); if (qrCode == null) { _logger.LogWarning($"QR code not found or doesn't belong to user - QRId: {qrId}, UserId: {userId}"); return false; } // Soft delete: mark as inactive instead of permanently deleting var update = Builders.Update.Set(q => q.IsActive, false); var result = await _context.QRCodeHistory.UpdateOneAsync( q => q.Id == qrId && q.UserId == userId, update); return result.ModifiedCount > 0; } catch (Exception ex) { _logger.LogError(ex, $"Error deleting QR from history - QRId: {qrId}, UserId: {userId}: {ex.Message}"); return false; } } public async Task GetQRCountThisMonthAsync(string userId) { try { var startOfMonth = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, 1); var endOfMonth = startOfMonth.AddMonths(1); var count = await _context.QRCodeHistory .CountDocumentsAsync(q => q.UserId == userId && q.CreatedAt >= startOfMonth && q.CreatedAt < endOfMonth); return (int)count; } catch (Exception ex) { _logger.LogError(ex, $"Error getting monthly QR count for user {userId}: {ex.Message}"); return 0; } } // MÉTODO REMOVIDO: ExtendAdFreeTimeAsync - não é mais necessário public async Task GetUserEmailAsync(string userId) { var user = await GetUserAsync(userId); return user?.Email ?? string.Empty; } public async Task MarkPremiumCancelledAsync(string userId, DateTime cancelledAt) { try { if (_context.Users == null) return; // Development mode without MongoDB var update = Builders.Update .Set(u => u.IsPremium, false) .Set(u => u.PremiumCancelledAt, cancelledAt) .Set(u => u.PremiumExpiresAt, null); await _context.Users.UpdateOneAsync(u => u.Id == userId, update); _logger.LogInformation($"Marked premium as cancelled for user {userId} at {cancelledAt}"); } catch (Exception ex) { _logger.LogError(ex, $"Error marking premium cancelled for user {userId}: {ex.Message}"); } } public async Task> GetUsersForHistoryCleanupAsync(DateTime cutoffDate) { try { if (_context.Users == null) return new List(); // Development mode without MongoDB return await _context.Users .Find(u => u.PremiumCancelledAt != null && u.PremiumCancelledAt < cutoffDate && u.QRHistoryIds.Count > 0) .ToListAsync(); } catch (Exception ex) { _logger.LogError(ex, $"Error getting users for history cleanup: {ex.Message}"); return new List(); } } public async Task DeleteUserHistoryAsync(string userId) { try { if (_context.Users == null || _context.QRCodeHistory == null) return; // Development mode without MongoDB var user = await GetUserAsync(userId); if (user?.QRHistoryIds?.Any() == true) { // Remover histórico de QR codes await _context.QRCodeHistory.DeleteManyAsync(qr => user.QRHistoryIds.Contains(qr.Id)); // Limpar lista de histórico do usuário var update = Builders.Update.Set(u => u.QRHistoryIds, new List()); await _context.Users.UpdateOneAsync(u => u.Id == userId, update); _logger.LogInformation($"Deleted history for user {userId}"); } } catch (Exception ex) { _logger.LogError(ex, $"Error deleting history for user {userId}: {ex.Message}"); } } public async Task ActivatePremiumStatus(string userId, string stripeSubscriptionId, DateTime expiryDate) { // Verifica se é uma nova assinatura (não renovação) var user = await GetUserAsync(userId); var isNewSubscription = user?.StripeSubscriptionId != stripeSubscriptionId; var updateBuilder = Builders.Update .Set(u => u.IsPremium, true) .Set(u => u.StripeSubscriptionId, stripeSubscriptionId) .Set(u => u.PremiumExpiresAt, expiryDate) .Unset(u => u.PremiumCancelledAt); // Se é nova assinatura, atualiza a data de início (para CDC 7 dias) if (isNewSubscription) { updateBuilder = updateBuilder.Set(u => u.SubscriptionStartedAt, DateTime.UtcNow); } await _context.Users.UpdateOneAsync(u => u.Id == userId, updateBuilder); _logger.LogInformation($"Activated premium for user {userId} (new subscription: {isNewSubscription})"); } public async Task DeactivatePremiumStatus(string stripeSubscriptionId) { var update = Builders.Update .Set(u => u.IsPremium, false) .Set(u => u.PremiumCancelledAt, DateTime.UtcNow); await _context.Users.UpdateOneAsync(u => u.StripeSubscriptionId == stripeSubscriptionId, update); _logger.LogInformation($"Deactivated premium for subscription {stripeSubscriptionId}"); } public async Task GetUserByStripeCustomerIdAsync(string customerId) { return await _context.Users.Find(u => u.StripeCustomerId == customerId).FirstOrDefaultAsync(); } public async Task UpdateUserStripeCustomerIdAsync(string userId, string stripeCustomerId) { var update = Builders.Update.Set(u => u.StripeCustomerId, stripeCustomerId); await _context.Users.UpdateOneAsync(u => u.Id == userId, update); } /// /// Get QR code by tracking ID (for analytics) /// public async Task GetQRByTrackingIdAsync(string trackingId) { try { return await _context.QRCodeHistory .Find(q => q.TrackingId == trackingId && q.IsActive) .FirstOrDefaultAsync(); } catch (Exception ex) { _logger.LogError(ex, "Error getting QR by tracking ID {TrackingId}: {ErrorMessage}", trackingId, ex.Message); return null; } } /// /// Increment scan count for QR code analytics /// public async Task IncrementQRScanCountAsync(string trackingId) { try { var filter = Builders.Filter.Eq(q => q.TrackingId, trackingId); var update = Builders.Update .Inc(q => q.ScanCount, 1) .Set(q => q.LastAccessedAt, DateTime.UtcNow); var result = await _context.QRCodeHistory.UpdateOneAsync(filter, update); if (result.ModifiedCount > 0) { _logger.LogDebug("QR scan count incremented - TrackingId: {TrackingId}", trackingId); } else { _logger.LogWarning("Failed to increment scan count - TrackingId not found: {TrackingId}", trackingId); } } catch (Exception ex) { _logger.LogError(ex, "Error incrementing scan count for {TrackingId}: {ErrorMessage}", trackingId, ex.Message); } } public async Task DeductCreditAsync(string userId) { try { var update = Builders.Update.Inc(u => u.Credits, -1); var result = await _context.Users.UpdateOneAsync(u => u.Id == userId && u.Credits > 0, update); return result.ModifiedCount > 0; } catch (Exception ex) { _logger.LogError(ex, $"Error deducting credit for user {userId}"); return false; } } public async Task AddCreditsAsync(string userId, int amount) { try { var update = Builders.Update.Inc(u => u.Credits, amount); var result = await _context.Users.UpdateOneAsync(u => u.Id == userId, update); return result.ModifiedCount > 0; } catch (Exception ex) { _logger.LogError(ex, $"Error adding credits for user {userId}"); return false; } } public async Task IncrementFreeUsageAsync(string userId) { try { // Limite de 5 QRs gratuitos vitalícios/iniciais var user = await GetUserAsync(userId); if (user == null || user.FreeQRsUsed >= 5) return false; var update = Builders.Update.Inc(u => u.FreeQRsUsed, 1); await _context.Users.UpdateOneAsync(u => u.Id == userId, update); return true; } catch (Exception ex) { _logger.LogError(ex, $"Error incrementing free usage for user {userId}"); return false; } } public async Task FindDuplicateQRAsync(string userId, string contentHash) { try { // Verifica se o hash existe na lista do usuário (rápido) var user = await GetUserAsync(userId); if (user == null || user.HistoryHashes == null || !user.HistoryHashes.Contains(contentHash)) { return null; } // Se existe, busca o objeto completo no histórico return await _context.QRCodeHistory .Find(q => q.UserId == userId && q.ContentHash == contentHash && q.IsActive) .SortByDescending(q => q.CreatedAt) .FirstOrDefaultAsync(); } catch (Exception ex) { _logger.LogError(ex, $"Error finding duplicate QR for user {userId}"); return null; } } public async Task CheckAnonymousLimitAsync(string ipAddress, string deviceId) { try { // Definição do limite: 1 por dia var limit = 1; var today = DateTime.UtcNow.Date; var tomorrow = today.AddDays(1); // Busca QRs gerados hoje por este IP OU DeviceId var count = await _context.QRCodeHistory .CountDocumentsAsync(q => q.UserId == null && // Apenas anônimos q.CreatedAt >= today && q.CreatedAt < tomorrow && (q.IpAddress == ipAddress || q.DeviceId == deviceId) ); return count < limit; } catch (Exception ex) { _logger.LogError(ex, $"Error checking anonymous limit for IP {ipAddress}"); // Em caso de erro no banco (timeout, etc), bloqueia por segurança ou libera? // Vamos liberar para não prejudicar UX em falha técnica momentânea, // mas logamos o erro. return true; } } public async Task RegisterAnonymousUsageAsync(string ipAddress, string deviceId, string qrId) { try { var update = Builders.Update .Set(q => q.IpAddress, ipAddress) .Set(q => q.DeviceId, deviceId); await _context.QRCodeHistory.UpdateOneAsync(q => q.Id == qrId, update); } catch (Exception ex) { _logger.LogError(ex, "Error registering anonymous usage"); } } public async Task<(string rawKey, string prefix)> GenerateApiKeyAsync(string userId, string keyName = "Default") { var rawKey = $"qr_{Guid.NewGuid():N}{Guid.NewGuid():N}"; var prefix = rawKey.Substring(0, 8); var hash = ComputeSha256Hash(rawKey); var keyConfig = new ApiKeyConfig { KeyHash = hash, Prefix = prefix, Name = keyName, CreatedAt = DateTime.UtcNow, IsActive = true }; var update = Builders.Update.Push(u => u.ApiKeys, keyConfig); await _context.Users.UpdateOneAsync(u => u.Id == userId, update); return (rawKey, prefix); } public async Task RevokeApiKeyAsync(string userId, string prefix) { var filter = Builders.Filter.Eq(u => u.Id, userId); var update = Builders.Update.Set("apiKeys.$[key].isActive", false); var arrayFilters = new List { new BsonDocumentArrayFilterDefinition(new BsonDocument("key.prefix", prefix)) }; var result = await _context.Users.UpdateOneAsync(filter, update, new UpdateOptions { ArrayFilters = arrayFilters }); return result.ModifiedCount > 0; } public async Task GetUserByApiKeyAsync(string rawKey) { var hash = ComputeSha256Hash(rawKey); var user = await _context.Users.Find(u => u.ApiKeys.Any(k => k.KeyHash == hash && k.IsActive)).FirstOrDefaultAsync(); if (user != null) { // Update last used timestamp (fire and forget) var update = Builders.Update.Set("apiKeys.$[key].lastUsedAt", DateTime.UtcNow); var arrayFilters = new List { new BsonDocumentArrayFilterDefinition(new BsonDocument("key.keyHash", hash)) }; _ = _context.Users.UpdateOneAsync(u => u.Id == user.Id, update, new UpdateOptions { ArrayFilters = arrayFilters }); } return user; } public async Task ActivateApiSubscriptionAsync( string userId, string stripeSubscriptionId, ApiPlanTier tier, DateTime periodEnd, string stripeCustomerId) { try { var update = Builders.Update .Set(u => u.ApiSubscription.Tier, tier) .Set(u => u.ApiSubscription.Status, "active") .Set(u => u.ApiSubscription.StripeSubscriptionId, stripeSubscriptionId) .Set(u => u.ApiSubscription.StripeCustomerId, stripeCustomerId) .Set(u => u.ApiSubscription.CurrentPeriodEnd, periodEnd) .Set(u => u.ApiSubscription.ActivatedAt, DateTime.UtcNow) .Set(u => u.ApiSubscription.CanceledAt, (DateTime?)null); await _context.Users.UpdateOneAsync(u => u.Id == userId, update); _logger.LogInformation("API subscription activated: user={UserId} tier={Tier}", userId, tier); } catch (Exception ex) { _logger.LogError(ex, "Error activating API subscription for user {UserId}", userId); } } public async Task GetUserByApiSubscriptionIdAsync(string stripeSubscriptionId) { try { return await _context.Users .Find(u => u.ApiSubscription.StripeSubscriptionId == stripeSubscriptionId) .FirstOrDefaultAsync(); } catch (Exception ex) { _logger.LogError(ex, "Error finding user by API subscription {SubId}", stripeSubscriptionId); return null; } } public async Task UpdateApiSubscriptionStatusAsync( string stripeSubscriptionId, string status, ApiPlanTier? newTier = null, DateTime? periodEnd = null) { try { var updateDef = Builders.Update .Set(u => u.ApiSubscription.Status, status); if (newTier.HasValue) updateDef = updateDef.Set(u => u.ApiSubscription.Tier, newTier.Value); if (periodEnd.HasValue) updateDef = updateDef.Set(u => u.ApiSubscription.CurrentPeriodEnd, periodEnd.Value); if (status == "canceled") updateDef = updateDef.Set(u => u.ApiSubscription.CanceledAt, DateTime.UtcNow); await _context.Users.UpdateOneAsync( u => u.ApiSubscription.StripeSubscriptionId == stripeSubscriptionId, updateDef); _logger.LogInformation("API subscription {SubId} status → {Status}", stripeSubscriptionId, status); } catch (Exception ex) { _logger.LogError(ex, "Error updating API subscription status {SubId}", stripeSubscriptionId); } } private string ComputeSha256Hash(string rawData) { using (var sha256Hash = System.Security.Cryptography.SHA256.Create()) { byte[] bytes = sha256Hash.ComputeHash(System.Text.Encoding.UTF8.GetBytes(rawData)); var builder = new System.Text.StringBuilder(); for (int i = 0; i < bytes.Length; i++) builder.Append(bytes[i].ToString("x2")); return builder.ToString(); } } } }