using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using System.Globalization; using UniVerse.Application.DTOs.Achievements; using UniVerse.Application.DTOs.Common; using UniVerse.Application.DTOs.Gamification; using UniVerse.Application.DTOs.Notifications; using UniVerse.Application.Interfaces; using UniVerse.Application.Mappings; using UniVerse.Domain.Entities; using UniVerse.Domain.Enums; using UniVerse.Infrastructure.Data; namespace UniVerse.Infrastructure.Services; public class GamificationService : IGamificationService { private readonly AppDbContext _db; private readonly INotificationService _notifications; private readonly ILogger _logger; private List? _levelThresholds; public GamificationService( AppDbContext db, INotificationService notifications, ILogger logger) { _db = db; _notifications = notifications; _logger = logger; } public async Task AwardCoinsAsync(int userId, int amount, CoinTransactionType type, int? reviewId = null, int? achievementId = null, string? description = null) { var user = await _db.Users.FindAsync(userId); if (user == null) return; user.Coins += amount; user.Xp += amount; _db.CoinTransactions.Add(new CoinTransaction { UserId = userId, Amount = amount, Type = type, ReviewId = reviewId, AchievementId = achievementId, Description = description }); await _db.SaveChangesAsync(); _logger.LogInformation("Awarded {Amount} coins to user {UserId} ({Type})", amount, userId, type); } public async Task CheckAndAwardAchievementsAsync(int userId) { var user = await _db.Users .Include(u => u.StudentProfile) .FirstOrDefaultAsync(u => u.Id == userId); if (user == null) return; var achievements = await _db.Achievements.OrderBy(a => a.Id).ToListAsync(); var existing = await _db.UserAchievements.Where(ua => ua.UserId == userId) .Select(ua => ua.AchievementId).ToListAsync(); var reviews = await _db.Reviews.CountAsync(r => r.UserId == userId); var registered = await _db.LectureEnrollments.CountAsync(e => e.UserId == userId); var attended = await _db.LectureEnrollments.CountAsync(e => e.UserId == userId && e.Attended); var activeRegistrations = await _db.LectureEnrollments .Include(e => e.Lecture) .CountAsync(e => e.UserId == userId && e.Lecture.StartsAt > DateTime.UtcNow); var earnedCoins = await _db.CoinTransactions .Where(ct => ct.UserId == userId && ct.Amount > 0) .SumAsync(ct => (int?)ct.Amount) ?? 0; var attendanceStreakWeeks = await CalculateAttendanceStreakWeeksAsync(userId); var profileCompleted = !string.IsNullOrWhiteSpace(user.DisplayName) && !string.IsNullOrWhiteSpace(user.AvatarUrl); var firstActivity = registered > 0 || reviews > 0 || attended > 0; foreach (var achievement in achievements.Where(a => !existing.Contains(a.Id))) { if (!TryParseCondition(achievement.Condition, out var type, out var value)) continue; var earned = type switch { "lectures_attended" => attended >= value, "reviews_written" => reviews >= value, "lectures_registered" => registered >= value, "active_registrations" => activeRegistrations >= value, "attendance_streak_weeks" => attendanceStreakWeeks >= value, "attended_registered" => attended >= value, "coins_earned" => earnedCoins >= value, "level_reached" => await CalculateLevelAsync(user.Xp) >= value, "profile_completed" => profileCompleted && value <= 1, "first_activity" => firstActivity && value <= 1, _ => false }; if (!earned) continue; _db.UserAchievements.Add(new UserAchievement { UserId = userId, AchievementId = achievement.Id }); if (achievement.CoinReward > 0) { await AwardCoinsAsync(userId, achievement.CoinReward, CoinTransactionType.AchievementReward, achievementId: achievement.Id, description: $"Achievement: {achievement.Name}"); earnedCoins += achievement.CoinReward; } await TryNotifyAchievementAsync(user, achievement); } await _db.SaveChangesAsync(); } private async Task TryNotifyAchievementAsync(User user, Achievement achievement) { try { var title = $"Новое достижение: {achievement.Name}"; var rewardText = achievement.CoinReward > 0 ? $" Награда: {achievement.CoinReward} монет." : string.Empty; var body = $"{achievement.Description ?? "Вы выполнили условие достижения."}{rewardText}"; await _notifications.CreateUserNotificationAsync(user.Id, "achievement", title, body); await _notifications.SendAsync(new NotificationMessage( NotificationChannels.Email, user.Email, title, $"Здравствуйте, {user.DisplayName ?? user.Email}!\n\nПоздравляем: вы получили достижение «{achievement.Name}» в UniVerse.\n\n{body}", user.DisplayName, new Dictionary { ["event"] = "achievement_earned", ["achievement_id"] = achievement.Id.ToString(), ["achievement_name"] = achievement.Name })); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to send achievement notification {AchievementId} to user {UserId}", achievement.Id, user.Id); } } private static bool TryParseCondition(string? condition, out string type, out int value) { type = string.Empty; value = 0; var parts = condition?.Split(':', 2, StringSplitOptions.TrimEntries); if (parts is not { Length: 2 } || string.IsNullOrWhiteSpace(parts[0])) return false; if (!int.TryParse(parts[1], out value)) return false; type = parts[0]; return value >= 0; } private async Task CalculateAttendanceStreakWeeksAsync(int userId) { var lectureDates = await _db.LectureEnrollments .Include(e => e.Lecture) .Where(e => e.UserId == userId && e.Attended) .Select(e => e.Lecture.StartsAt) .ToListAsync(); var weekStarts = lectureDates .Select(GetIsoWeekStart) .Distinct() .OrderBy(d => d) .ToList(); var best = 0; var current = 0; DateOnly? previous = null; foreach (var weekStart in weekStarts) { current = previous.HasValue && weekStart.DayNumber - previous.Value.DayNumber == 7 ? current + 1 : 1; best = Math.Max(best, current); previous = weekStart; } return best; } private static DateOnly GetIsoWeekStart(DateTime date) { var isoYear = ISOWeek.GetYear(date); var isoWeek = ISOWeek.GetWeekOfYear(date); return DateOnly.FromDateTime(ISOWeek.ToDateTime(isoYear, isoWeek, DayOfWeek.Monday)); } public async Task CalculateLevelAsync(int xp) { var thresholds = await GetLevelThresholdsAsync(); return thresholds .Where(t => xp >= t.RequiredXp) .OrderBy(t => t.RequiredXp) .ThenBy(t => t.Level) .LastOrDefault()?.Level ?? thresholds[0].Level; } public async Task GetLevelProgressAsync(int xp) { var thresholds = await GetLevelThresholdsAsync(); var current = thresholds .Where(t => xp >= t.RequiredXp) .OrderBy(t => t.RequiredXp) .ThenBy(t => t.Level) .LastOrDefault() ?? thresholds[0]; var next = thresholds .Where(t => t.RequiredXp > current.RequiredXp) .OrderBy(t => t.RequiredXp) .ThenBy(t => t.Level) .FirstOrDefault(); return new LevelProgressDto(current.RequiredXp, next?.RequiredXp); } private async Task> GetLevelThresholdsAsync() { if (_levelThresholds is { Count: > 0 }) return _levelThresholds; _levelThresholds = await _db.LevelThresholds .AsNoTracking() .OrderBy(t => t.RequiredXp) .ThenBy(t => t.Level) .ToListAsync(); if (_levelThresholds.Count == 0) _levelThresholds.Add(new LevelThreshold { Level = 1, RequiredXp = 0 }); return _levelThresholds; } public async Task> GetUserAchievementsAsync(int userId) => await _db.UserAchievements.Include(ua => ua.Achievement) .Where(ua => ua.UserId == userId).OrderByDescending(ua => ua.AwardedAt) .Select(ua => ua.ToDto()).ToListAsync(); public async Task> GetTransactionsAsync(int userId, PaginationRequest pagination) { var query = _db.CoinTransactions.Where(ct => ct.UserId == userId); var total = await query.CountAsync(); var items = await query.OrderByDescending(ct => ct.CreatedAt) .Skip((pagination.Page - 1) * pagination.PageSize).Take(pagination.PageSize) .Select(ct => ct.ToDto()).ToListAsync(); return PagedResult.Create(items, total, pagination.Page, pagination.PageSize); } }