using Microsoft.AspNetCore.Mvc; using DramaLing.Api.Models.Entities; using DramaLing.Api.Models.DTOs; using DramaLing.Api.Contracts.Repositories; using DramaLing.Api.Contracts.Services.Review; using Microsoft.AspNetCore.Authorization; using DramaLing.Api.Utils; using DramaLing.Api.Services; using DramaLing.Api.Services.Storage; using DramaLing.Api.Data; using Microsoft.EntityFrameworkCore; namespace DramaLing.Api.Controllers; [Route("api/flashcards")] [Authorize] public class FlashcardsController : BaseController { private readonly IFlashcardRepository _flashcardRepository; private readonly IReviewService _reviewService; private readonly DramaLingDbContext _context; private readonly IImageStorageService _imageStorageService; public FlashcardsController( IFlashcardRepository flashcardRepository, IReviewService reviewService, DramaLingDbContext context, IImageStorageService imageStorageService, IAuthService authService, ILogger logger) : base(logger, authService) { _flashcardRepository = flashcardRepository; _reviewService = reviewService; _context = context; _imageStorageService = imageStorageService; } private async Task GetImageUrlAsync(string? relativePath) { if (string.IsNullOrEmpty(relativePath)) return null; // 確保路徑包含 examples/ 前綴 var fullPath = relativePath.StartsWith("examples/") ? relativePath : $"examples/{relativePath}"; return await _imageStorageService.GetImageUrlAsync(fullPath); } [HttpGet] public async Task GetFlashcards( [FromQuery] string? search = null, [FromQuery] bool favoritesOnly = false) { try { var userId = await GetCurrentUserIdAsync(); var flashcards = await _flashcardRepository.GetByUserIdAsync(userId, search, favoritesOnly); // 獲取用戶的複習記錄 var flashcardIds = flashcards.Select(f => f.Id).ToList(); var reviews = await _context.FlashcardReviews .Where(fr => fr.UserId == userId && flashcardIds.Contains(fr.FlashcardId)) .ToDictionaryAsync(fr => fr.FlashcardId); // 重構為 foreach 迴圈,支援異步 URL 處理 var flashcardList = new List(); foreach (var f in flashcards) { reviews.TryGetValue(f.Id, out var review); // 取得主要圖片的相對路徑並轉換為完整 URL var primaryImageRelativePath = f.FlashcardExampleImages .Where(fei => fei.IsPrimary) .Select(fei => fei.ExampleImage.RelativePath) .FirstOrDefault(); var primaryImageUrl = await GetImageUrlAsync(primaryImageRelativePath); flashcardList.Add(new { f.Id, f.Word, f.Translation, f.Definition, f.PartOfSpeech, f.Pronunciation, f.Example, f.ExampleTranslation, f.IsFavorite, f.Synonyms, DifficultyLevelNumeric = f.DifficultyLevelNumeric, CEFR = CEFRHelper.ToString(f.DifficultyLevelNumeric), f.CreatedAt, f.UpdatedAt, // 添加複習相關屬性 NextReviewDate = review?.NextReviewDate ?? DateTime.UtcNow.AddDays(1), TimesReviewed = review?.TotalCorrectCount + review?.TotalWrongCount + review?.TotalSkipCount ?? 0, MasteryLevel = review?.SuccessCount ?? 0, // 添加圖片相關屬性 HasExampleImage = f.FlashcardExampleImages.Any(), PrimaryImageUrl = primaryImageUrl }); } var flashcardData = new { Flashcards = flashcardList, Count = flashcards.Count() }; return SuccessResponse(flashcardData); } catch (UnauthorizedAccessException) { return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401); } catch (Exception ex) { _logger.LogError(ex, "Error getting flashcards"); return ErrorResponse("INTERNAL_ERROR", "載入詞卡失敗"); } } [HttpPost] public async Task CreateFlashcard([FromBody] CreateFlashcardRequest request) { try { if (!ModelState.IsValid) { return HandleModelStateErrors(); } var userId = await GetCurrentUserIdAsync(); var flashcard = new Flashcard { Id = Guid.NewGuid(), UserId = userId, Word = request.Word, Translation = request.Translation, Definition = request.Definition ?? "", PartOfSpeech = request.PartOfSpeech, Pronunciation = request.Pronunciation, Example = request.Example, ExampleTranslation = request.ExampleTranslation, Synonyms = request.Synonyms, // 儲存 AI 生成的同義詞 DifficultyLevelNumeric = CEFRHelper.ToNumeric(request.CEFR ?? "A0"), CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; await _flashcardRepository.AddAsync(flashcard); await _flashcardRepository.SaveChangesAsync(); return SuccessResponse(flashcard, "詞卡創建成功"); } catch (UnauthorizedAccessException) { return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401); } catch (Exception ex) { _logger.LogError(ex, "Error creating flashcard"); return ErrorResponse("INTERNAL_ERROR", "創建詞卡失敗"); } } [HttpGet("{id}")] public async Task GetFlashcard(Guid id) { try { var userId = await GetCurrentUserIdAsync(); var flashcard = await _flashcardRepository.GetByUserIdAndFlashcardIdAsync(userId, id); if (flashcard == null) { return ErrorResponse("NOT_FOUND", "詞卡不存在", null, 404); } // 獲取複習記錄 var review = await _context.FlashcardReviews .FirstOrDefaultAsync(fr => fr.UserId == userId && fr.FlashcardId == id); // 格式化返回數據,保持與列表 API 一致 var flashcardData = new { flashcard.Id, flashcard.Word, flashcard.Translation, flashcard.Definition, flashcard.PartOfSpeech, flashcard.Pronunciation, flashcard.Example, flashcard.ExampleTranslation, flashcard.IsFavorite, flashcard.Synonyms, DifficultyLevelNumeric = flashcard.DifficultyLevelNumeric, CEFR = CEFRHelper.ToString(flashcard.DifficultyLevelNumeric), flashcard.CreatedAt, flashcard.UpdatedAt, // 添加複習相關屬性(與列表 API 一致) NextReviewDate = review?.NextReviewDate ?? DateTime.UtcNow.AddDays(1), TimesReviewed = review?.TotalCorrectCount + review?.TotalWrongCount + review?.TotalSkipCount ?? 0, MasteryLevel = review?.SuccessCount ?? 0, // 添加圖片相關屬性 HasExampleImage = flashcard.FlashcardExampleImages.Any(), PrimaryImageUrl = await GetImageUrlAsync(flashcard.FlashcardExampleImages .Where(fei => fei.IsPrimary) .Select(fei => fei.ExampleImage.RelativePath) .FirstOrDefault()), // 保留完整的圖片關聯數據供前端使用 FlashcardExampleImages = flashcard.FlashcardExampleImages }; return SuccessResponse(flashcardData); } catch (UnauthorizedAccessException) { return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401); } catch (Exception ex) { _logger.LogError(ex, "Error getting flashcard {FlashcardId}", id); return ErrorResponse("INTERNAL_ERROR", "取得詞卡失敗"); } } [HttpPut("{id}")] public async Task UpdateFlashcard(Guid id, [FromBody] CreateFlashcardRequest request) { try { if (!ModelState.IsValid) { return HandleModelStateErrors(); } var userId = await GetCurrentUserIdAsync(); var flashcard = await _flashcardRepository.GetByUserIdAndFlashcardIdAsync(userId, id); if (flashcard == null) { return ErrorResponse("NOT_FOUND", "詞卡不存在", null, 404); } // 更新詞卡資訊 flashcard.Word = request.Word; flashcard.Translation = request.Translation; flashcard.Definition = request.Definition ?? ""; flashcard.PartOfSpeech = request.PartOfSpeech; flashcard.Pronunciation = request.Pronunciation; flashcard.Example = request.Example; flashcard.ExampleTranslation = request.ExampleTranslation; flashcard.UpdatedAt = DateTime.UtcNow; await _flashcardRepository.UpdateAsync(flashcard); await _flashcardRepository.SaveChangesAsync(); return SuccessResponse(flashcard, "詞卡更新成功"); } catch (UnauthorizedAccessException) { return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401); } catch (Exception ex) { _logger.LogError(ex, "Error updating flashcard {FlashcardId}", id); return ErrorResponse("INTERNAL_ERROR", "更新詞卡失敗"); } } [HttpDelete("{id}")] public async Task DeleteFlashcard(Guid id) { try { var userId = await GetCurrentUserIdAsync(); var flashcard = await _flashcardRepository.GetByUserIdAndFlashcardIdAsync(userId, id); if (flashcard == null) { return ErrorResponse("NOT_FOUND", "詞卡不存在", null, 404); } await _flashcardRepository.DeleteAsync(flashcard); await _flashcardRepository.SaveChangesAsync(); return SuccessResponse(new { Id = id }, "詞卡已刪除"); } catch (UnauthorizedAccessException) { return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401); } catch (Exception ex) { _logger.LogError(ex, "Error deleting flashcard {FlashcardId}", id); return ErrorResponse("INTERNAL_ERROR", "刪除詞卡失敗"); } } [HttpPost("{id}/favorite")] public async Task ToggleFavorite(Guid id) { try { var userId = await GetCurrentUserIdAsync(); var flashcard = await _flashcardRepository.GetByUserIdAndFlashcardIdAsync(userId, id); if (flashcard == null) { return ErrorResponse("NOT_FOUND", "詞卡不存在", null, 404); } flashcard.IsFavorite = !flashcard.IsFavorite; flashcard.UpdatedAt = DateTime.UtcNow; await _flashcardRepository.UpdateAsync(flashcard); await _flashcardRepository.SaveChangesAsync(); var result = new { Id = flashcard.Id, IsFavorite = flashcard.IsFavorite }; var message = flashcard.IsFavorite ? "已加入收藏" : "已取消收藏"; return SuccessResponse(result, message); } catch (UnauthorizedAccessException) { return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401); } catch (Exception ex) { _logger.LogError(ex, "Error toggling favorite for flashcard {FlashcardId}", id); return ErrorResponse("INTERNAL_ERROR", "切換收藏狀態失敗"); } } [HttpGet("due")] public async Task GetDueFlashcards( [FromQuery] int limit = 10, [FromQuery] bool includeToday = true, [FromQuery] bool includeOverdue = true, [FromQuery] bool favoritesOnly = false) { try { var userId = await GetCurrentUserIdAsync(); var query = new DueFlashcardsQuery { Limit = limit, IncludeToday = includeToday, IncludeOverdue = includeOverdue, FavoritesOnly = favoritesOnly }; var response = await _reviewService.GetDueFlashcardsAsync(userId, query); return Ok(response); } catch (UnauthorizedAccessException) { return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401); } catch (Exception ex) { _logger.LogError(ex, "Error getting due flashcards"); return ErrorResponse("INTERNAL_ERROR", "載入待複習詞卡失敗"); } } [HttpPost("{id}/review")] public async Task SubmitReview(Guid id, [FromBody] ReviewRequest request) { try { if (!ModelState.IsValid) { return HandleModelStateErrors(); } var userId = await GetCurrentUserIdAsync(); var response = await _reviewService.SubmitReviewAsync(userId, id, request); return Ok(response); } catch (UnauthorizedAccessException) { return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401); } catch (Exception ex) { _logger.LogError(ex, "Error submitting review for flashcard {FlashcardId}", id); return ErrorResponse("INTERNAL_ERROR", "提交複習結果失敗"); } } [HttpPost("{id}/mastered")] public async Task MarkWordMastered(Guid id) { try { var userId = await GetCurrentUserIdAsync(); var response = await _reviewService.MarkWordMasteredAsync(userId, id); return Ok(response); } catch (UnauthorizedAccessException) { return ErrorResponse("UNAUTHORIZED", "認證失敗", null, 401); } catch (Exception ex) { _logger.LogError(ex, "Error marking flashcard {FlashcardId} as mastered", id); return ErrorResponse("INTERNAL_ERROR", "標記詞彙掌握失敗"); } } } // DTO 類別 public class CreateFlashcardRequest { public string Word { get; set; } = string.Empty; public string Translation { get; set; } = string.Empty; public string Definition { get; set; } = string.Empty; public string PartOfSpeech { get; set; } = string.Empty; public string Pronunciation { get; set; } = string.Empty; public string Example { get; set; } = string.Empty; public string? ExampleTranslation { get; set; } public string? Synonyms { get; set; } // AI 生成的同義詞 (JSON 字串) public string? CEFR { get; set; } = string.Empty; }