using System.Text.Json; using Mindforge.API.Exceptions; using Mindforge.API.Models.Flashcards; using Mindforge.API.Models.Requests; using Mindforge.API.Services.Interfaces; namespace Mindforge.API.Services { public class FlashcardService : IFlashcardService { private readonly IAgentService _agentService; private readonly IGiteaService _giteaService; private readonly IFlashcardRepository _flashcardRepository; private readonly ILogger _logger; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; public FlashcardService( IAgentService agentService, IGiteaService giteaService, IFlashcardRepository flashcardRepository, ILogger logger) { _agentService = agentService; _giteaService = giteaService; _flashcardRepository = flashcardRepository; _logger = logger; } public async Task> GenerateFlashcardsAsync(FlashcardGenerateRequest request) { if (request.FilePaths.Count == 0) { throw new UserException("Selecione ao menos um arquivo para gerar flashcards."); } if (request.Amount is < 10 or > 30) { throw new UserException("A quantidade de flashcards deve estar entre 10 e 30 por arquivo."); } var uniqueFilePaths = request.FilePaths .Where(path => !string.IsNullOrWhiteSpace(path)) .Select(path => path.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); if (uniqueFilePaths.Count == 0) { throw new UserException("Nenhum caminho de arquivo valido foi enviado."); } var generatedLibraries = new List(); foreach (var filePath in uniqueFilePaths) { var fileContent = await _giteaService.GetFileContentAsync(filePath); var fileName = Path.GetFileName(filePath); var subject = ExtractSubject(filePath); var cards = await GenerateCardsForFileAsync(filePath, fileContent, request.Amount, request.Difficulty); var library = await _flashcardRepository.UpsertLibraryAsync( filePath, fileName, subject, request.Difficulty.ToString(), cards); generatedLibraries.Add(library); } return generatedLibraries; } public Task> GetLibrariesAsync() { return _flashcardRepository.GetLibrariesAsync(); } public Task GetLibraryByIdAsync(long libraryId) { return _flashcardRepository.GetLibraryByIdAsync(libraryId); } public async Task> BuildReviewSessionAsync(FlashcardReviewSessionRequest request) { var libraryIds = (request.LibraryIds ?? []) .Where(id => id > 0) .Distinct() .ToList(); if (libraryIds.Count == 0) { throw new UserException("Selecione ao menos uma biblioteca para iniciar a revisao."); } return await _flashcardRepository.GetCardsForLibrariesAsync(libraryIds); } private async Task> GenerateCardsForFileAsync( string filePath, string fileContent, int amount, FlashcardDifficulty difficulty) { var difficultyInstruction = difficulty switch { FlashcardDifficulty.Hard => "Crie perguntas mais desafiadoras, focando diferencas finas e cobrancas de prova.", _ => "Crie perguntas diretas e objetivas para facilitar memorizacao inicial." }; var systemPrompt = "Voce e um assistente educacional especializado em flashcards.\n" + $"Gere exatamente {amount} flashcards em Portugues do Brasil.\n" + "Cada item precisa conter:\n" + "- front: pergunta ou gatilho curto\n" + "- back: resposta clara, correta e objetiva\n" + $"{difficultyInstruction}\n" + "Responda apenas com JSON valido, sem markdown, no formato:\n" + "{\n" + " \"flashcards\": [\n" + " { \"front\": \"texto\", \"back\": \"texto\" }\n" + " ]\n" + "}"; var userPrompt = $""" Arquivo: {filePath} Conteudo: {fileContent} """; var rawResult = await _agentService.ProcessRequestAsync(systemPrompt, userPrompt); var cards = ParseFlashcardsFromJson(rawResult); if (cards.Count < amount) { throw new Exception($"Quantidade insuficiente de flashcards gerados para {filePath}. Esperado: {amount}. Recebido: {cards.Count}."); } if (cards.Count > amount) { _logger.LogWarning( "Quantidade de flashcards acima do solicitado para {FilePath}. Solicitado: {Amount}. Recebido: {Generated}.", filePath, amount, cards.Count); cards = cards.Take(amount).ToList(); } for (var i = 0; i < cards.Count; i++) { cards[i].Position = i + 1; } return cards; } private static List ParseFlashcardsFromJson(string rawResult) { var cleanJson = StripJsonCodeBlock(rawResult); var payload = JsonSerializer.Deserialize(cleanJson, JsonOptions); if (payload?.Flashcards is { Count: > 0 }) { return payload.Flashcards .Where(card => !string.IsNullOrWhiteSpace(card.Front) && !string.IsNullOrWhiteSpace(card.Back)) .Select(card => new FlashcardDraftCard { Front = card.Front.Trim(), Back = card.Back.Trim() }) .ToList(); } var directList = JsonSerializer.Deserialize>(cleanJson, JsonOptions); if (directList is { Count: > 0 }) { return directList .Where(card => !string.IsNullOrWhiteSpace(card.Front) && !string.IsNullOrWhiteSpace(card.Back)) .Select(card => new FlashcardDraftCard { Front = card.Front.Trim(), Back = card.Back.Trim() }) .ToList(); } throw new Exception("Resposta de flashcards invalida. O modelo nao retornou JSON estruturado."); } private static string StripJsonCodeBlock(string value) { var trimmed = value.Trim(); if (!trimmed.StartsWith("```", StringComparison.Ordinal)) { return trimmed; } var lines = trimmed.Split('\n'); if (lines.Length <= 2) { return trimmed.Trim('`', '\r', '\n'); } return string.Join('\n', lines.Skip(1).Take(lines.Length - 2)).Trim(); } private static string ExtractSubject(string filePath) { var normalized = filePath.Replace('\\', '/'); var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries); if (segments.Length == 0) { return "Geral"; } var concursosIndex = Array.FindIndex( segments, segment => segment.Equals("concursos", StringComparison.OrdinalIgnoreCase) || segment.Equals("concurso", StringComparison.OrdinalIgnoreCase)); if (concursosIndex >= 0 && concursosIndex + 1 < segments.Length) { return segments[concursosIndex + 1]; } return segments[0]; } private class FlashcardJsonPayload { public List Flashcards { get; set; } = []; } private class FlashcardJsonCard { public string Front { get; set; } = string.Empty; public string Back { get; set; } = string.Empty; } } }