using DramaLing.Api.Data;
using DramaLing.Api.Models.Configuration;
using DramaLing.Api.Models.Entities;
using DramaLing.Api.Services.Monitoring;
using DramaLing.Api.Services.AI.Gemini;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using System.Diagnostics;
using System.Text.Json;
namespace DramaLing.Api.Services;
///
/// 選項詞彙庫服務實作
/// 提供基於 CEFR 等級、詞性和字數的智能選項生成
///
public class OptionsVocabularyService : IOptionsVocabularyService
{
private readonly DramaLingDbContext _context;
private readonly IMemoryCache _cache;
private readonly ILogger _logger;
private readonly OptionsVocabularyOptions _options;
private readonly OptionsVocabularyMetrics _metrics;
private readonly IGeminiClient _geminiClient;
public OptionsVocabularyService(
DramaLingDbContext context,
IMemoryCache cache,
ILogger logger,
IOptions options,
OptionsVocabularyMetrics metrics,
IGeminiClient geminiClient)
{
_context = context;
_cache = cache;
_logger = logger;
_options = options.Value;
_metrics = metrics;
_geminiClient = geminiClient;
}
///
/// 生成智能干擾選項
///
public async Task> GenerateDistractorsAsync(
string targetWord,
string cefrLevel,
string partOfSpeech,
int count = 3)
{
var distractorsWithDetails = await GenerateDistractorsWithDetailsAsync(
targetWord, cefrLevel, partOfSpeech, count);
return distractorsWithDetails.Select(v => v.Word).ToList();
}
///
/// 生成智能干擾選項(含詳細資訊)
///
public async Task> GenerateDistractorsWithDetailsAsync(
string targetWord,
string cefrLevel,
string partOfSpeech,
int count = 3)
{
var stopwatch = Stopwatch.StartNew();
try
{
// 記錄請求指標
_metrics.RecordGenerationRequest(cefrLevel, partOfSpeech, count);
_logger.LogInformation("Generating {Count} distractors for word '{Word}' (CEFR: {CEFR}, PartOfSpeech: {PartOfSpeech})",
count, targetWord, cefrLevel, partOfSpeech);
// 1. 首先檢查資料庫中是否已有該詞彙的混淆選項
var cacheKey = $"quiz_options_{targetWord.ToLowerInvariant()}_{cefrLevel}_{partOfSpeech}";
if (_cache.TryGetValue(cacheKey, out List? cachedOptions) && cachedOptions != null)
{
_logger.LogInformation("✅ Using cached distractors for '{Word}': {Distractors}",
targetWord, string.Join(", ", cachedOptions.Select(d => d.Word)));
return cachedOptions.Take(count).ToList();
}
// 2. 檢查資料庫中是否有現有選項
var targetWordLower = targetWord.ToLower();
var existingOptions = await _context.OptionsVocabularies
.Where(ov => ov.CEFRLevel == cefrLevel &&
ov.PartOfSpeech == partOfSpeech &&
ov.IsActive &&
ov.Word.ToLower() != targetWordLower)
.Take(count * 2) // 取更多選項以供隨機選擇
.ToListAsync();
if (existingOptions.Count >= count)
{
var selectedExisting = existingOptions
.OrderBy(x => Guid.NewGuid()) // 隨機排序
.Take(count)
.ToList();
// 快取結果
_cache.Set(cacheKey, selectedExisting, TimeSpan.FromHours(1));
_logger.LogInformation("📚 Using existing database distractors for '{Word}': {Distractors}",
targetWord, string.Join(", ", selectedExisting.Select(d => d.Word)));
return selectedExisting;
}
_logger.LogInformation("💡 Insufficient database options ({ExistingCount}/{RequiredCount}), proceeding to AI generation for '{Word}'",
existingOptions.Count, count, targetWord);
// 3. 使用 AI 生成新的混淆選項
var aiGeneratedOptions = await GenerateOptionsWithAI(targetWord, cefrLevel, partOfSpeech, count);
// 4. 保存新生成的選項到資料庫
if (aiGeneratedOptions.Any())
{
await _context.OptionsVocabularies.AddRangeAsync(aiGeneratedOptions);
await _context.SaveChangesAsync();
// 快取結果
_cache.Set(cacheKey, aiGeneratedOptions, TimeSpan.FromHours(1));
}
_logger.LogInformation("🤖 Successfully generated {Count} AI distractors for '{Word}': {Distractors}",
aiGeneratedOptions.Count, targetWord,
string.Join(", ", aiGeneratedOptions.Select(d => d.Word)));
// 記錄生成完成指標
stopwatch.Stop();
_metrics.RecordGenerationDuration(stopwatch.Elapsed, aiGeneratedOptions.Count);
return aiGeneratedOptions;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating distractors for word '{Word}'", targetWord);
_metrics.RecordError("generation_failed", "GenerateDistractorsWithDetailsAsync");
// 降級到固定選項
return GetFallbackDistractors(targetWord, cefrLevel, partOfSpeech, count);
}
}
///
/// 檢查詞彙庫是否有足夠的詞彙支援選項生成
///
public async Task HasSufficientVocabularyAsync(string cefrLevel, string partOfSpeech)
{
try
{
var allowedLevels = GetAllowedCEFRLevels(cefrLevel);
var count = await _context.OptionsVocabularies
.Where(v => v.IsActive &&
allowedLevels.Contains(v.CEFRLevel) &&
v.PartOfSpeech == partOfSpeech)
.CountAsync();
var hasSufficient = count >= _options.MinimumVocabularyThreshold;
_logger.LogDebug("Vocabulary count for CEFR: {CEFR}, PartOfSpeech: {PartOfSpeech}: {Count} (sufficient: {HasSufficient})",
cefrLevel, partOfSpeech, count, hasSufficient);
return hasSufficient;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error checking vocabulary sufficiency for CEFR: {CEFR}, PartOfSpeech: {PartOfSpeech}",
cefrLevel, partOfSpeech);
return false;
}
}
///
/// 獲取允許的 CEFR 等級(包含相鄰等級)
///
private static List GetAllowedCEFRLevels(string targetLevel)
{
var levels = new[] { "A1", "A2", "B1", "B2", "C1", "C2" };
var targetIndex = Array.IndexOf(levels, targetLevel);
if (targetIndex == -1)
{
// 如果不是標準 CEFR 等級,只返回原等級
return new List { targetLevel };
}
var allowed = new List { targetLevel };
// 加入相鄰等級(允許難度稍有差異)
if (targetIndex > 0)
allowed.Add(levels[targetIndex - 1]);
if (targetIndex < levels.Length - 1)
allowed.Add(levels[targetIndex + 1]);
return allowed;
}
///
/// 獲取允許的 CEFR 數字等級(包含相鄰等級)
///
private static List GetAllowedCEFRLevelsNumeric(int targetLevelNumeric)
{
var allowed = new List { targetLevelNumeric };
// 加入相鄰等級(允許難度稍有差異)
if (targetLevelNumeric > 1)
allowed.Add(targetLevelNumeric - 1);
if (targetLevelNumeric < 6)
allowed.Add(targetLevelNumeric + 1);
return allowed;
}
///
/// 使用 AI 生成混淆選項
///
private async Task> GenerateOptionsWithAI(
string targetWord,
string cefrLevel,
string partOfSpeech,
int count)
{
try
{
var prompt = $@"Generate {count} vocabulary words that would be good wrong answers for a multiple choice quiz about the word ""{targetWord}"".
Requirements:
- CEFR level: {cefrLevel}
- Part of speech: {partOfSpeech}
- The words should be plausible but clearly different from ""{targetWord}""
- Avoid obviously wrong answers
- Make the words challenging but fair for language learners
Please respond with ONLY a JSON array of strings, like: [""word1"", ""word2"", ""word3""]";
var response = await _geminiClient.CallGeminiAPIAsync(prompt);
// 解析 AI 回應
var cleanedResponse = ExtractJsonFromResponse(response);
var generatedWords = JsonSerializer.Deserialize(cleanedResponse);
if (generatedWords == null || generatedWords.Length == 0)
{
_logger.LogWarning("AI returned empty or invalid response for word '{Word}'", targetWord);
return GetFallbackDistractors(targetWord, cefrLevel, partOfSpeech, count);
}
// 轉換為 OptionsVocabulary 實體,並過濾掉與目標詞彙相同的詞彙
var targetWordLower = targetWord.ToLower();
var filteredWords = generatedWords
.Where(word => !string.IsNullOrWhiteSpace(word) &&
word.Trim().ToLower() != targetWordLower)
.Take(count);
var options = filteredWords.Select(word => new OptionsVocabulary
{
Id = Guid.NewGuid(),
Word = word.Trim(),
CEFRLevel = cefrLevel,
PartOfSpeech = partOfSpeech,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
}).ToList();
_logger.LogInformation("Filtered AI generated words from {OriginalCount} to {FilteredCount} options for '{TargetWord}'",
generatedWords.Length, options.Count, targetWord);
// 計算字數長度
foreach (var option in options)
{
option.CalculateWordLength();
}
return options;
}
catch (Exception ex)
{
_logger.LogError(ex, "AI generation failed for word '{Word}', falling back to fixed options", targetWord);
return GetFallbackDistractors(targetWord, cefrLevel, partOfSpeech, count);
}
}
///
/// 降級固定選項(當 AI 生成失敗時使用)
///
private List GetFallbackDistractors(
string targetWord,
string cefrLevel,
string partOfSpeech,
int count)
{
// 根據詞性提供不同的固定選項(避免常見目標詞彙)
var fallbackWords = partOfSpeech.ToLower() switch
{
"verb" => new[] { "accomplish", "construct", "transform", "establish", "generate", "implement", "maintain", "organize", "validate", "coordinate" },
"noun" => new[] { "solution", "approach", "framework", "component", "structure", "mechanism", "principle", "strategy", "technique", "procedure" },
"adjective" => new[] { "efficient", "comprehensive", "innovative", "sophisticated", "fundamental", "essential", "remarkable", "outstanding", "significant", "substantial" },
"adverb" => new[] { "thoroughly", "efficiently", "precisely", "consistently", "systematically", "effectively", "accurately", "appropriately", "specifically", "particularly" },
_ => new[] { "element", "aspect", "feature", "component", "factor", "criteria", "parameter", "attribute", "characteristic", "specification" }
};
var targetWordLower = targetWord.ToLower();
var selectedWords = fallbackWords
.Where(word => word.ToLower() != targetWordLower)
.Take(count * 2) // 取更多選項用於隨機選擇
.OrderBy(x => Guid.NewGuid()) // 隨機排序
.Take(count);
_logger.LogInformation("Using fallback distractors for '{TargetWord}': {FallbackWords}",
targetWord, string.Join(", ", selectedWords));
return selectedWords
.Select(word => new OptionsVocabulary
{
Id = Guid.NewGuid(),
Word = word,
CEFRLevel = cefrLevel,
PartOfSpeech = partOfSpeech,
IsActive = true,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
})
.ToList();
}
///
/// 從 AI 回應中提取 JSON 內容
///
private string ExtractJsonFromResponse(string response)
{
// 移除可能的 markdown 代碼塊標記
var cleaned = response.Replace("```json", "").Replace("```", "").Trim();
// 找到第一個 [ 和最後一個 ]
var startIndex = cleaned.IndexOf('[');
var endIndex = cleaned.LastIndexOf(']');
if (startIndex >= 0 && endIndex >= 0 && endIndex > startIndex)
{
return cleaned.Substring(startIndex, endIndex - startIndex + 1);
}
return cleaned;
}
}