using GermanApp.Application.DTOs; using GermanApp.Application.Interfaces; using GermanApp.Domain.Entities; using GermanApp.Domain.Interfaces; using Microsoft.EntityFrameworkCore; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace GermanApp.Application.Services; /// /// Service for generating user progress reports. /// Part of the Application layer. /// public class UserReportService : IUserReportService { private readonly IRepository _userRepository; private readonly IUserProgressRepository _userProgressRepository; private readonly ILessonRepository _lessonRepository; private readonly ILevelRepository _levelRepository; private readonly IQuizRepository _quizRepository; private readonly IStoryProgressRepository _storyProgressRepository; public UserReportService( IRepository userRepository, IUserProgressRepository userProgressRepository, ILessonRepository lessonRepository, ILevelRepository levelRepository, IQuizRepository quizRepository, IStoryProgressRepository storyProgressRepository) { _userRepository = userRepository; _userProgressRepository = userProgressRepository; _lessonRepository = lessonRepository; _levelRepository = levelRepository; _quizRepository = quizRepository; _storyProgressRepository = storyProgressRepository; } /// /// Generates a progress report for a specific user. /// /// The user ID /// Cancellation token /// User progress report public async Task GenerateUserReportAsync(int userId, CancellationToken cancellationToken = default) { var user = await _userRepository.GetByIdAsync(userId, cancellationToken); if (user == null) throw new ArgumentException("User not found", nameof(userId)); var userProgress = await _userProgressRepository.GetByUserAsync(userId, cancellationToken); var allLessons = await _lessonRepository.GetAllAsync(cancellationToken); var totalLessonsCompleted = userProgress.Count(up => up.IsCompleted); var totalQuizzesCompleted = userProgress.Count(up => up.IsCompleted && up.QuizScore > 0); var averageQuizScore = userProgress.Where(up => up.QuizScore > 0).Select(up => up.QuizScore).DefaultIfEmpty().Average(); // Find last activity date var lastActivityDate = userProgress .OrderByDescending(up => up.LastAttemptDate) .FirstOrDefault()?.LastAttemptDate ?? user.CreatedAt; return new UserProgressReportDto( user.Id, user.Username, user.Email, user.CurrentLevel, totalLessonsCompleted, totalQuizzesCompleted, averageQuizScore, user.TotalPoints, user.Streak, lastActivityDate); } /// /// Generates progress reports for all users. /// /// Cancellation token /// List of all user progress reports public async Task> GenerateAllUserReportsAsync(CancellationToken cancellationToken = default) { var users = await _userRepository.GetAllAsync(cancellationToken); var reports = new List(); foreach (var user in users) { var report = await GenerateUserReportAsync(user.Id, cancellationToken); reports.Add(report); } return reports; } /// /// Gets lesson completion data for a user. /// /// The user ID /// Cancellation token /// List of lesson completion data public async Task> GetUserLessonProgressAsync(int userId, CancellationToken cancellationToken = default) { var userProgress = await _userProgressRepository.GetByUserAsync(userId, cancellationToken); var allLessons = await _lessonRepository.GetAllAsync(cancellationToken); var allLevels = await _levelRepository.GetAllAsync(cancellationToken); var levelDict = allLevels.ToDictionary(l => l.Id, l => l.Code); return userProgress .Select(up => new LessonCompletionDto( up.LessonId, up.Lesson?.Title ?? "Unknown", up.Lesson?.LevelId != null && levelDict.TryGetValue(up.Lesson.LevelId, out var levelCode) ? levelCode : "Unknown", up.IsCompleted, up.IsCompleted ? up.LastAttemptDate : null)) .OrderBy(l => l.LevelCode) .ThenBy(l => l.LessonTitle) .ToList(); } /// /// Gets quiz results for a user. /// /// The user ID /// Cancellation token /// List of quiz results public async Task> GetUserQuizResultsAsync(int userId, CancellationToken cancellationToken = default) { var userProgress = await _userProgressRepository.GetByUserAsync(userId, cancellationToken); return userProgress .Where(up => up.QuizScore > 0) .Select(up => new UserQuizResultDto( up.Lesson?.Id ?? 0, // Using LessonId as QuizId proxy (simplified) up.Lesson?.Title ?? "Unknown Quiz", up.QuizScore, 80, // Default passing score up.QuizScore >= 80, up.LastAttemptDate)) .OrderByDescending(r => r.AttemptDate) .ToList(); } /// /// Exports user reports as CSV. /// /// Cancellation token /// CSV content as string public async Task ExportReportsAsCsvAsync(CancellationToken cancellationToken = default) { var reports = await GenerateAllUserReportsAsync(cancellationToken); var sb = new StringBuilder(); // Header row sb.AppendLine("User ID,Username,Email,Current Level,Lessons Completed,Quizzes Completed,Average Score,Total Points,Streak,Last Activity"); // Data rows foreach (var report in reports) { sb.AppendLine($"{EscapeCsv(report.UserId.ToString())},{EscapeCsv(report.Username)},{EscapeCsv(report.Email)},{EscapeCsv(report.CurrentLevel)},{report.TotalLessonsCompleted},{report.TotalQuizzesCompleted},{report.AverageQuizScore:F2},{report.TotalPoints},{report.CurrentStreak},{EscapeCsv(report.LastActivityDate.ToString("yyyy-MM-dd HH:mm:ss"))}"); } return sb.ToString(); } /// /// Escapes a value for CSV output. /// /// The value to escape /// Escaped value private static string EscapeCsv(string value) { if (value == null) return string.Empty; // If the value contains commas, quotes, or newlines, wrap it in quotes and escape quotes if (value.Contains(",") || value.Contains("\"") || value.Contains("\n") || value.Contains("\r")) { return $"\"{value.Replace("\"", "\"\"")}\""; } return value; } }