using DramaLing.Api.Models.DTOs; using DramaLing.Api.Models.Entities; using DramaLing.Api.Contracts.Repositories; using DramaLing.Api.Controllers; using DramaLing.Api.Utils; using DramaLing.Api.Services; using DramaLing.Api.Data; using DramaLing.Api.Services.AI.Utils; using DramaLing.Api.Contracts.Services.Review; using DramaLing.Api.Contracts.Services.Core; namespace DramaLing.Api.Services.Review; public class ReviewService : IReviewService { private readonly IFlashcardReviewRepository _reviewRepository; private readonly IOptionsVocabularyService _optionsService; private readonly ILogger _logger; public ReviewService( IFlashcardReviewRepository reviewRepository, IOptionsVocabularyService optionsService, ILogger logger) { _reviewRepository = reviewRepository; _optionsService = optionsService; _logger = logger; } public async Task> GetDueFlashcardsAsync(Guid userId, DueFlashcardsQuery query) { try { var dueFlashcards = await _reviewRepository.GetDueFlashcardsAsync(userId, query); var (todayDue, overdue, totalReviews) = await _reviewRepository.GetReviewStatsAsync(userId); // 為每張詞卡生成 quizOptions var flashcardDataTasks = dueFlashcards.Select(async item => { // 生成混淆選項 var generatedQuizOptions = await _optionsService.GenerateDistractorsAsync( item.Flashcard.Word, CEFRHelper.ToString(item.Flashcard.DifficultyLevelNumeric), item.Flashcard.PartOfSpeech ?? "noun", 3); return new { // 基本詞卡信息 (匹配 api_seeds.json 格式) id = item.Flashcard.Id.ToString(), word = item.Flashcard.Word, translation = item.Flashcard.Translation, definition = item.Flashcard.Definition ?? "", partOfSpeech = item.Flashcard.PartOfSpeech ?? "", pronunciation = item.Flashcard.Pronunciation ?? "", example = item.Flashcard.Example ?? "", exampleTranslation = item.Flashcard.ExampleTranslation ?? "", isFavorite = item.Flashcard.IsFavorite, difficultyLevelNumeric = item.Flashcard.DifficultyLevelNumeric, cefr = CEFRHelper.ToString(item.Flashcard.DifficultyLevelNumeric), createdAt = item.Flashcard.CreatedAt.ToString("yyyy-MM-ddTHH:mm:ssZ"), updatedAt = item.Flashcard.UpdatedAt.ToString("yyyy-MM-ddTHH:mm:ssZ"), // 圖片相關 (暫時設為預設值,因為需要額外查詢) hasExampleImage = false, primaryImageUrl = (string?)null, // 同義詞(從資料庫讀取,使用 AI 工具類解析) synonyms = SynonymsParser.ParseSynonymsJson(item.Flashcard.Synonyms), // 測驗選項 (AI 生成的混淆選項) quizOptions = generatedQuizOptions, // 複習相關信息 (新增) reviewInfo = item.Review != null ? new { successCount = item.Review.SuccessCount, nextReviewDate = item.Review.NextReviewDate.ToString("yyyy-MM-ddTHH:mm:ssZ"), lastReviewDate = item.Review.LastReviewDate?.ToString("yyyy-MM-ddTHH:mm:ssZ"), totalCorrectCount = item.Review.TotalCorrectCount, totalWrongCount = item.Review.TotalWrongCount, totalSkipCount = item.Review.TotalSkipCount, isOverdue = item.Review.NextReviewDate < DateTime.UtcNow.Date, daysSinceLastReview = item.Review.LastReviewDate.HasValue ? (int)(DateTime.UtcNow - item.Review.LastReviewDate.Value).TotalDays : 0 } : new { successCount = 0, nextReviewDate = DateTime.UtcNow.AddDays(1).ToString("yyyy-MM-ddTHH:mm:ssZ"), lastReviewDate = (string?)null, totalCorrectCount = 0, totalWrongCount = 0, totalSkipCount = 0, isOverdue = false, daysSinceLastReview = 0 } }; }); // 等待所有異步任務完成 var flashcardData = await Task.WhenAll(flashcardDataTasks); var response = new { flashcards = flashcardData, count = flashcardData.Length, metadata = new { todayDue = todayDue, overdue = overdue, totalReviews = totalReviews, studyStreak = 0 // 暫時設為0,未來可實作 } }; return new ApiResponse { Success = true, Data = response, Message = null, Timestamp = DateTime.UtcNow }; } catch (Exception ex) { _logger.LogError(ex, "Error getting due flashcards for user {UserId}", userId); throw; } } public async Task> SubmitReviewAsync(Guid userId, Guid flashcardId, ReviewRequest request) { try { // 獲取或創建複習記錄 var review = await _reviewRepository.GetOrCreateReviewAsync(userId, flashcardId); // 處理複習結果 var result = ProcessReview(review, request); // 更新記錄 await _reviewRepository.UpdateReviewAsync(review); return new ApiResponse { Success = true, Data = result, Timestamp = DateTime.UtcNow }; } catch (Exception ex) { _logger.LogError(ex, "Error submitting review for flashcard {FlashcardId}", flashcardId); throw; } } public async Task> GetReviewStatsAsync(Guid userId, string period = "today") { try { var (todayDue, overdue, totalReviews) = await _reviewRepository.GetReviewStatsAsync(userId); var stats = new ReviewStats { TodayReviewed = 0, // TODO: 實作當日複習統計 TodayDue = todayDue, Overdue = overdue, TotalReviews = totalReviews, AverageAccuracy = 0.0, // TODO: 實作正確率統計 StudyStreak = 0 // TODO: 實作連續學習天數 }; return new ApiResponse { Success = true, Data = stats, Timestamp = DateTime.UtcNow }; } catch (Exception ex) { _logger.LogError(ex, "Error getting review stats for user {UserId}", userId); throw; } } public async Task> MarkWordMasteredAsync(Guid userId, Guid flashcardId) { try { // 使用 repository 的 GetOrCreate 方法 var review = await _reviewRepository.GetOrCreateReviewAsync(userId, flashcardId); // 簡化邏輯:直接標記為掌握 review.SuccessCount++; review.TotalCorrectCount++; review.LastSuccessDate = DateTime.UtcNow; review.LastReviewDate = DateTime.UtcNow; // 核心算法:間隔 = 2^成功次數 天,最大180天 var intervalDays = (int)Math.Pow(2, review.SuccessCount); var maxInterval = 180; var finalInterval = Math.Min(intervalDays, maxInterval); review.NextReviewDate = DateTime.UtcNow.AddDays(finalInterval); review.UpdatedAt = DateTime.UtcNow; // 使用 repository 的更新方法 await _reviewRepository.UpdateReviewAsync(review); return new ApiResponse { Success = true, Data = new { nextReviewDate = review.NextReviewDate.ToString("yyyy-MM-ddTHH:mm:ssZ"), intervalDays = finalInterval, successCount = review.SuccessCount, message = "詞彙已標記為掌握" }, Message = "詞彙掌握狀態已更新", Timestamp = DateTime.UtcNow }; } catch (Exception ex) { _logger.LogError(ex, "Error marking flashcard {FlashcardId} as mastered for user {UserId}", flashcardId, userId); throw; } } /// /// 處理複習結果的核心算法 /// private ReviewResult ProcessReview(FlashcardReview review, ReviewRequest request) { if (request.WasSkipped) { // 跳過: 不改變成功次數,明天再複習 review.TotalSkipCount++; review.NextReviewDate = DateTime.UtcNow.AddDays(1); } else { // 根據信心度判斷是否答對 (0=不熟悉答錯, 1-2=答對) var isCorrect = request.Confidence >= 1; if (isCorrect) { // 答對: 增加成功次數,計算新間隔 review.SuccessCount++; review.TotalCorrectCount++; review.LastSuccessDate = DateTime.UtcNow; // 核心公式: 間隔 = 2^成功次數 天 var intervalDays = (int)Math.Pow(2, review.SuccessCount); var maxInterval = 180; // 最大半年 var finalInterval = Math.Min(intervalDays, maxInterval); review.NextReviewDate = DateTime.UtcNow.AddDays(finalInterval); } else { // 答錯: 重置成功次數,明天再複習 review.SuccessCount = 0; review.TotalWrongCount++; review.NextReviewDate = DateTime.UtcNow.AddDays(1); } } review.LastReviewDate = DateTime.UtcNow; review.UpdatedAt = DateTime.UtcNow; return new ReviewResult { FlashcardId = review.FlashcardId, NewSuccessCount = review.SuccessCount, NextReviewDate = review.NextReviewDate, IntervalDays = (int)(review.NextReviewDate - DateTime.UtcNow).TotalDays, MasteryLevelChange = 0.0, // 暫時設為0 IsNewRecord = review.CreatedAt == review.UpdatedAt }; } }