using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using DramaLing.Api.Data; using DramaLing.Api.Models.Entities; using DramaLing.Api.Services; using Microsoft.AspNetCore.Authorization; namespace DramaLing.Api.Controllers; [ApiController] [Route("api/[controller]")] [Authorize] public class StudyController : ControllerBase { private readonly DramaLingDbContext _context; private readonly IAuthService _authService; private readonly ILogger _logger; public StudyController( DramaLingDbContext context, IAuthService authService, ILogger logger) { _context = context; _authService = authService; _logger = logger; } /// /// 獲取待複習的詞卡 (支援 /frontend/app/learn/page.tsx) /// [HttpGet("due-cards")] public async Task GetDueCards( [FromQuery] int limit = 50, [FromQuery] string? mode = null, [FromQuery] bool includeNew = true) { try { var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization); if (userId == null) return Unauthorized(new { Success = false, Error = "Invalid token" }); var today = DateTime.Today; var query = _context.Flashcards .Include(f => f.CardSet) .Where(f => f.UserId == userId); // 篩選到期和新詞卡 if (includeNew) { // 包含到期詞卡和新詞卡 query = query.Where(f => f.NextReviewDate <= today || f.Repetitions == 0); } else { // 只包含到期詞卡 query = query.Where(f => f.NextReviewDate <= today); } var dueCards = await query.Take(limit * 2).ToListAsync(); // 取更多用於排序 // 計算優先級並排序 var cardsWithPriority = dueCards.Select(card => new { Card = card, Priority = ReviewPriorityCalculator.CalculatePriority( card.NextReviewDate, card.EasinessFactor, card.Repetitions ), IsDue = ReviewPriorityCalculator.ShouldReview(card.NextReviewDate), DaysOverdue = Math.Max(0, (today - card.NextReviewDate).Days) }).OrderByDescending(x => x.Priority).Take(limit); var result = cardsWithPriority.Select(x => new { x.Card.Id, x.Card.Word, x.Card.Translation, x.Card.Definition, x.Card.PartOfSpeech, x.Card.Pronunciation, x.Card.Example, x.Card.ExampleTranslation, x.Card.MasteryLevel, x.Card.NextReviewDate, x.Card.DifficultyLevel, CardSet = new { x.Card.CardSet.Name, x.Card.CardSet.Color }, x.Priority, x.IsDue, x.DaysOverdue }).ToList(); // 統計資訊 var totalDue = await _context.Flashcards .Where(f => f.UserId == userId && f.NextReviewDate <= today) .CountAsync(); var totalCards = await _context.Flashcards .Where(f => f.UserId == userId) .CountAsync(); var newCards = await _context.Flashcards .Where(f => f.UserId == userId && f.Repetitions == 0) .CountAsync(); return Ok(new { Success = true, Data = new { Cards = result, TotalDue = totalDue, TotalCards = totalCards, NewCards = newCards } }); } catch (Exception ex) { _logger.LogError(ex, "Error fetching due cards for user"); return StatusCode(500, new { Success = false, Error = "Failed to fetch due cards", Timestamp = DateTime.UtcNow }); } } /// /// 開始學習會話 /// [HttpPost("sessions")] public async Task CreateStudySession([FromBody] CreateStudySessionRequest request) { try { var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization); if (userId == null) return Unauthorized(new { Success = false, Error = "Invalid token" }); // 基本驗證 if (string.IsNullOrEmpty(request.Mode) || !new[] { "flip", "quiz", "fill", "listening", "speaking" }.Contains(request.Mode)) { return BadRequest(new { Success = false, Error = "Invalid study mode" }); } if (request.CardIds == null || request.CardIds.Count == 0) { return BadRequest(new { Success = false, Error = "Card IDs are required" }); } if (request.CardIds.Count > 50) { return BadRequest(new { Success = false, Error = "Cannot study more than 50 cards in one session" }); } // 驗證詞卡是否屬於用戶 var userCards = await _context.Flashcards .Where(f => f.UserId == userId && request.CardIds.Contains(f.Id)) .CountAsync(); if (userCards != request.CardIds.Count) { return BadRequest(new { Success = false, Error = "Some cards not found or not accessible" }); } // 建立學習會話 var session = new StudySession { Id = Guid.NewGuid(), UserId = userId.Value, SessionType = request.Mode, TotalCards = request.CardIds.Count, StartedAt = DateTime.UtcNow }; _context.StudySessions.Add(session); await _context.SaveChangesAsync(); // 獲取詞卡詳細資訊 var cards = await _context.Flashcards .Include(f => f.CardSet) .Where(f => f.UserId == userId && request.CardIds.Contains(f.Id)) .ToListAsync(); // 按照請求的順序排列 var orderedCards = request.CardIds .Select(id => cards.FirstOrDefault(c => c.Id == id)) .Where(c => c != null) .ToList(); return Ok(new { Success = true, Data = new { SessionId = session.Id, SessionType = request.Mode, Cards = orderedCards.Select(c => new { c.Id, c.Word, c.Translation, c.Definition, c.PartOfSpeech, c.Pronunciation, c.Example, c.ExampleTranslation, c.MasteryLevel, c.EasinessFactor, c.Repetitions, CardSet = new { c.CardSet.Name, c.CardSet.Color } }), TotalCards = orderedCards.Count, StartedAt = session.StartedAt }, Message = $"Study session started with {orderedCards.Count} cards" }); } catch (Exception ex) { _logger.LogError(ex, "Error creating study session"); return StatusCode(500, new { Success = false, Error = "Failed to create study session", Timestamp = DateTime.UtcNow }); } } /// /// 記錄學習結果 (支援 SM-2 算法) /// [HttpPost("sessions/{sessionId}/record")] public async Task RecordStudyResult( Guid sessionId, [FromBody] RecordStudyResultRequest request) { try { var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization); if (userId == null) return Unauthorized(new { Success = false, Error = "Invalid token" }); // 基本驗證 if (request.QualityRating < 1 || request.QualityRating > 5) { return BadRequest(new { Success = false, Error = "Quality rating must be between 1 and 5" }); } // 驗證學習會話 var session = await _context.StudySessions .FirstOrDefaultAsync(s => s.Id == sessionId && s.UserId == userId); if (session == null) { return NotFound(new { Success = false, Error = "Study session not found" }); } // 驗證詞卡 var flashcard = await _context.Flashcards .FirstOrDefaultAsync(f => f.Id == request.FlashcardId && f.UserId == userId); if (flashcard == null) { return NotFound(new { Success = false, Error = "Flashcard not found" }); } // 計算新的 SM-2 參數 var sm2Input = new SM2Input( request.QualityRating, flashcard.EasinessFactor, flashcard.Repetitions, flashcard.IntervalDays ); var sm2Result = SM2Algorithm.Calculate(sm2Input); // 記錄學習結果 var studyRecord = new StudyRecord { Id = Guid.NewGuid(), UserId = userId.Value, FlashcardId = request.FlashcardId, SessionId = sessionId, StudyMode = session.SessionType, QualityRating = request.QualityRating, ResponseTimeMs = request.ResponseTimeMs, UserAnswer = request.UserAnswer, IsCorrect = request.IsCorrect, PreviousEasinessFactor = sm2Input.EasinessFactor, NewEasinessFactor = sm2Result.EasinessFactor, PreviousIntervalDays = sm2Input.IntervalDays, NewIntervalDays = sm2Result.IntervalDays, PreviousRepetitions = sm2Input.Repetitions, NewRepetitions = sm2Result.Repetitions, NextReviewDate = sm2Result.NextReviewDate, StudiedAt = DateTime.UtcNow }; _context.StudyRecords.Add(studyRecord); // 更新詞卡的 SM-2 參數 flashcard.EasinessFactor = sm2Result.EasinessFactor; flashcard.Repetitions = sm2Result.Repetitions; flashcard.IntervalDays = sm2Result.IntervalDays; flashcard.NextReviewDate = sm2Result.NextReviewDate; flashcard.MasteryLevel = SM2Algorithm.CalculateMastery(sm2Result.Repetitions, sm2Result.EasinessFactor); flashcard.TimesReviewed++; if (request.IsCorrect) flashcard.TimesCorrect++; flashcard.LastReviewedAt = DateTime.UtcNow; await _context.SaveChangesAsync(); return Ok(new { Success = true, Data = new { RecordId = studyRecord.Id, NextReviewDate = sm2Result.NextReviewDate.ToString("yyyy-MM-dd"), NewIntervalDays = sm2Result.IntervalDays, NewMasteryLevel = flashcard.MasteryLevel, EasinessFactor = sm2Result.EasinessFactor, Repetitions = sm2Result.Repetitions, QualityDescription = SM2Algorithm.GetQualityDescription(request.QualityRating) }, Message = $"Study record saved. Next review in {sm2Result.IntervalDays} day(s)" }); } catch (Exception ex) { _logger.LogError(ex, "Error recording study result"); return StatusCode(500, new { Success = false, Error = "Failed to record study result", Timestamp = DateTime.UtcNow }); } } /// /// 完成學習會話 /// [HttpPost("sessions/{sessionId}/complete")] public async Task CompleteStudySession( Guid sessionId, [FromBody] CompleteStudySessionRequest request) { try { var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization); if (userId == null) return Unauthorized(new { Success = false, Error = "Invalid token" }); // 驗證會話 var session = await _context.StudySessions .FirstOrDefaultAsync(s => s.Id == sessionId && s.UserId == userId); if (session == null) { return NotFound(new { Success = false, Error = "Study session not found" }); } // 計算會話統計 var sessionRecords = await _context.StudyRecords .Where(r => r.SessionId == sessionId && r.UserId == userId) .ToListAsync(); var correctCount = sessionRecords.Count(r => r.IsCorrect); var averageResponseTime = sessionRecords.Any(r => r.ResponseTimeMs.HasValue) ? (int)sessionRecords.Where(r => r.ResponseTimeMs.HasValue).Average(r => r.ResponseTimeMs!.Value) : 0; // 更新會話 session.EndedAt = DateTime.UtcNow; session.CorrectCount = correctCount; session.DurationSeconds = request.DurationSeconds; session.AverageResponseTimeMs = averageResponseTime; // 更新或建立每日統計 var today = DateOnly.FromDateTime(DateTime.Today); var dailyStats = await _context.DailyStats .FirstOrDefaultAsync(ds => ds.UserId == userId && ds.Date == today); if (dailyStats == null) { dailyStats = new DailyStats { Id = Guid.NewGuid(), UserId = userId.Value, Date = today }; _context.DailyStats.Add(dailyStats); } dailyStats.WordsStudied += sessionRecords.Count; dailyStats.WordsCorrect += correctCount; dailyStats.StudyTimeSeconds += request.DurationSeconds; dailyStats.SessionCount++; await _context.SaveChangesAsync(); // 計算會話統計 var accuracy = sessionRecords.Count > 0 ? (int)Math.Round((double)correctCount / sessionRecords.Count * 100) : 0; var averageTimePerCard = request.DurationSeconds > 0 && sessionRecords.Count > 0 ? request.DurationSeconds / sessionRecords.Count : 0; return Ok(new { Success = true, Data = new { SessionId = sessionId, TotalCards = session.TotalCards, CardsStudied = sessionRecords.Count, CorrectAnswers = correctCount, AccuracyPercentage = accuracy, DurationSeconds = request.DurationSeconds, AverageTimePerCard = averageTimePerCard, AverageResponseTimeMs = averageResponseTime, StartedAt = session.StartedAt, EndedAt = session.EndedAt }, Message = $"Study session completed! {correctCount}/{sessionRecords.Count} correct ({accuracy}%)" }); } catch (Exception ex) { _logger.LogError(ex, "Error completing study session"); return StatusCode(500, new { Success = false, Error = "Failed to complete study session", Timestamp = DateTime.UtcNow }); } } /// /// 獲取智能複習排程 /// [HttpGet("schedule")] public async Task GetReviewSchedule( [FromQuery] bool includePlan = true, [FromQuery] bool includeStats = true) { try { var userId = await _authService.GetUserIdFromTokenAsync(Request.Headers.Authorization); if (userId == null) return Unauthorized(new { Success = false, Error = "Invalid token" }); // 獲取用戶設定 var settings = await _context.UserSettings .FirstOrDefaultAsync(s => s.UserId == userId); var dailyGoal = settings?.DailyGoal ?? 20; // 獲取所有詞卡 var allCards = await _context.Flashcards .Where(f => f.UserId == userId) .ToListAsync(); var today = DateTime.Today; // 分類詞卡 var dueToday = allCards.Where(c => c.NextReviewDate == today).ToList(); var overdue = allCards.Where(c => c.NextReviewDate < today && c.Repetitions > 0).ToList(); var upcoming = allCards.Where(c => c.NextReviewDate > today && c.NextReviewDate <= today.AddDays(7)).ToList(); var newCards = allCards.Where(c => c.Repetitions == 0).ToList(); // 建立回應物件 var responseData = new Dictionary { ["Schedule"] = new { DueToday = dueToday.Count, Overdue = overdue.Count, Upcoming = upcoming.Count, NewCards = newCards.Count } }; // 生成學習計劃 if (includePlan) { var recommendedCards = overdue.Take(dailyGoal / 2) .Concat(dueToday.Take(dailyGoal / 3)) .Concat(newCards.Take(Math.Min(5, dailyGoal / 4))) .Take(dailyGoal) .Select(c => new { c.Id, c.Word, c.Translation, c.MasteryLevel, c.NextReviewDate, PriorityReason = c.Repetitions == 0 ? "new_card" : c.NextReviewDate < today ? "overdue" : "due_today" }); responseData["StudyPlan"] = new { RecommendedCards = recommendedCards, Breakdown = new { Overdue = Math.Min(overdue.Count, dailyGoal / 2), DueToday = Math.Min(dueToday.Count, dailyGoal / 3), NewCards = Math.Min(newCards.Count, 5) }, EstimatedTimeMinutes = recommendedCards.Count() * 1, DailyGoal = dailyGoal }; } // 計算統計 if (includeStats) { responseData["Statistics"] = new { TotalCards = allCards.Count, MasteredCards = allCards.Count(c => c.MasteryLevel >= 80), LearningCards = allCards.Count(c => c.MasteryLevel >= 40 && c.MasteryLevel < 80), NewCardsCount = newCards.Count, AverageMastery = allCards.Count > 0 ? (int)allCards.Average(c => c.MasteryLevel) : 0, RetentionRate = allCards.Count(c => c.Repetitions > 0) > 0 ? (int)Math.Round((double)allCards.Count(c => c.MasteryLevel >= 60) / allCards.Count(c => c.Repetitions > 0) * 100) : 0 }; } return Ok(new { Success = true, Data = responseData }); } catch (Exception ex) { _logger.LogError(ex, "Error fetching review schedule"); return StatusCode(500, new { Success = false, Error = "Failed to fetch review schedule", Timestamp = DateTime.UtcNow }); } } } // Request DTOs public class CreateStudySessionRequest { public string Mode { get; set; } = string.Empty; // flip, quiz, fill, listening, speaking public List CardIds { get; set; } = new(); } public class RecordStudyResultRequest { public Guid FlashcardId { get; set; } public int QualityRating { get; set; } // 1-5 public int? ResponseTimeMs { get; set; } public string? UserAnswer { get; set; } public bool IsCorrect { get; set; } } public class CompleteStudySessionRequest { public int DurationSeconds { get; set; } }