using Microsoft.EntityFrameworkCore; using UniVerse.Application.DTOs.Common; using UniVerse.Application.DTOs.Lectures; using UniVerse.Application.DTOs.Users; using UniVerse.Application.Interfaces; using UniVerse.Application.Mappings; using UniVerse.Domain.Enums; using UniVerse.Domain.Exceptions; using UniVerse.Domain.Services; using UniVerse.Infrastructure.Data; namespace UniVerse.Infrastructure.Services; public class UserService : IUserService { private readonly AppDbContext _db; private readonly IGamificationService _gamification; public UserService(AppDbContext db, IGamificationService gamification) { _db = db; _gamification = gamification; } public async Task GetByIdAsync(int id) { var user = await _db.Users .Include(u => u.Roles) .FirstOrDefaultAsync(u => u.Id == id) ?? throw new NotFoundException("User", id); return user.ToDto(await _gamification.CalculateLevelAsync(user.Xp)); } public async Task UpdateProfileAsync(int id, UpdateUserRequest request) { var user = await _db.Users .Include(u => u.Roles) .FirstOrDefaultAsync(u => u.Id == id) ?? throw new NotFoundException("User", id); if (request.DisplayName != null) user.DisplayName = request.DisplayName; if (request.AvatarUrl != null) user.AvatarUrl = request.AvatarUrl; user.UpdatedAt = DateTime.UtcNow; await _db.SaveChangesAsync(); await _gamification.CheckAndAwardAchievementsAsync(id); return user.ToDto(await _gamification.CalculateLevelAsync(user.Xp)); } public async Task GetStatsAsync(int id) { var user = await _db.Users.FindAsync(id) ?? throw new NotFoundException("User", id); var totalLectures = await _db.LectureEnrollments.CountAsync(e => e.UserId == id); var attended = await _db.LectureEnrollments.CountAsync(e => e.UserId == id && e.Attended); var reviews = await _db.Reviews.CountAsync(r => r.UserId == id); var achievements = await _db.UserAchievements.CountAsync(ua => ua.UserId == id); var activeEnrollments = await _db.LectureEnrollments .CountAsync(e => e.UserId == id && !e.Attended); var level = await _gamification.CalculateLevelAsync(user.Xp); var levelProgress = await _gamification.GetLevelProgressAsync(user.Xp); var slotLimit = EnrollmentSlotPolicy.GetLimitForLevel(level); var slotRules = EnrollmentSlotPolicy.Rules .Select(rule => new EnrollmentSlotRuleDto(rule.Level, rule.Slots)) .ToList(); return new UserStatsDto( totalLectures, attended, reviews, user.Xp, user.Coins, level, achievements, levelProgress.CurrentLevelXp, levelProgress.NextLevelXp, activeEnrollments, slotLimit, slotRules ); } public async Task GetAdminDashboardStatsAsync() { var usersCount = await _db.Users .CountAsync(user => !user.Roles.Any(role => role.Role == UserRole.Teacher)); var lecturesCount = await _db.Lectures.CountAsync(); var enrollmentsCount = await _db.LectureEnrollments.CountAsync(); var pendingReviewsCount = await _db.Reviews.CountAsync(review => review.LlmStatus == ReviewLlmStatus.Pending); return new AdminDashboardStatsDto(usersCount, lecturesCount, enrollmentsCount, pendingReviewsCount); } public async Task> GetEnrollmentsAsync(int id, PaginationRequest pagination) { if (!await _db.Users.AnyAsync(u => u.Id == id)) throw new NotFoundException("User", id); var query = _db.LectureEnrollments .Where(e => e.UserId == id) .Include(e => e.Lecture) .ThenInclude(l => l.Course) .Include(e => e.Lecture) .ThenInclude(l => l.Teacher) .Include(e => e.Lecture) .ThenInclude(l => l.Location) .Include(e => e.Lecture) .ThenInclude(l => l.Enrollments); var total = await query.CountAsync(); var enrollments = await query .OrderBy(e => e.Lecture.StartsAt) .Skip((pagination.Page - 1) * pagination.PageSize) .Take(pagination.PageSize) .ToListAsync(); return PagedResult.Create( enrollments.Select(e => e.Lecture.ToDto(isEnrolled: true)).ToList(), total, pagination.Page, pagination.PageSize); } public async Task> GetAllAsync(UserFilterRequest filter) { var query = _db.Users.AsQueryable(); if (!string.IsNullOrEmpty(filter.Search)) { var search = filter.Search.ToLower(); query = query.Where(u => u.Email.ToLower().Contains(search) || (u.DisplayName != null && u.DisplayName.ToLower().Contains(search))); } query = query.Include(u => u.Roles); if (filter.Role.HasValue) { var role = filter.Role.Value; query = query.Where(u => u.Roles.Count == 1 && u.Roles.Any(ur => ur.Role == role)); } if (filter.IsActive.HasValue) query = query.Where(u => u.IsActive == filter.IsActive.Value); var total = await query.CountAsync(); var users = await query .OrderByDescending(u => u.CreatedAt) .Skip((filter.Page - 1) * filter.PageSize) .Take(filter.PageSize) .ToListAsync(); var items = new List(users.Count); foreach (var user in users) items.Add(user.ToDto(await _gamification.CalculateLevelAsync(user.Xp))); return PagedResult.Create(items, total, filter.Page, filter.PageSize); } public async Task SetRolesAsync(int id, IReadOnlyCollection roles) { var normalizedRoles = roles.Distinct().ToList(); if (normalizedRoles.Count == 0) throw new ForbiddenException("At least one role is required."); var user = await _db.Users .Include(u => u.Roles) .FirstOrDefaultAsync(u => u.Id == id) ?? throw new NotFoundException("User", id); var existing = user.Roles.Select(r => r.Role).ToHashSet(); var toRemove = user.Roles.Where(r => !normalizedRoles.Contains(r.Role)).ToList(); foreach (var item in toRemove) user.Roles.Remove(item); var toAdd = normalizedRoles.Where(r => !existing.Contains(r)).ToList(); foreach (var role in toAdd) user.Roles.Add(new Domain.Entities.UserRoleAssignment { UserId = user.Id, Role = role }); await EnsureProfilesForRolesAsync(user.Id, normalizedRoles); user.UpdatedAt = DateTime.UtcNow; await _db.SaveChangesAsync(); } public async Task SetActiveAsync(int id, bool isActive) { var user = await _db.Users.FindAsync(id) ?? throw new NotFoundException("User", id); user.IsActive = isActive; user.UpdatedAt = DateTime.UtcNow; await _db.SaveChangesAsync(); } private async Task EnsureProfilesForRolesAsync(int userId, IReadOnlyCollection roles) { if (roles.Contains(UserRole.Student)) { var hasStudentProfile = await _db.StudentProfiles.AnyAsync(p => p.UserId == userId); if (!hasStudentProfile) _db.StudentProfiles.Add(new Domain.Entities.StudentProfile { UserId = userId }); } if (roles.Contains(UserRole.Teacher)) { var hasTeacherProfile = await _db.TeacherProfiles.AnyAsync(p => p.UserId == userId); if (!hasTeacherProfile) _db.TeacherProfiles.Add(new Domain.Entities.TeacherProfile { UserId = userId }); } } }