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;
}
}