using Dapper; using Mindforge.API.Models.Flashcards; using Mindforge.API.Services.Interfaces; using Npgsql; namespace Mindforge.API.Repositories { public class FlashcardRepository : IFlashcardRepository { private const string DefaultConnectionString = "Host=iris.haven;Database=mindforge;Username=root;Password=root"; private readonly string _connectionString; public FlashcardRepository(IConfiguration configuration) { _connectionString = configuration.GetConnectionString("MindforgeDb") ?? configuration["MINDFORGE_DB_CONNECTION"] ?? DefaultConnectionString; } public async Task EnsureSchemaAsync() { const string sql = """ CREATE TABLE IF NOT EXISTS flashcard_libraries ( id BIGSERIAL PRIMARY KEY, file_path TEXT NOT NULL UNIQUE, file_name TEXT NOT NULL, subject TEXT NOT NULL, difficulty TEXT NOT NULL, card_count INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS flashcards ( id BIGSERIAL PRIMARY KEY, library_id BIGINT NOT NULL REFERENCES flashcard_libraries(id) ON DELETE CASCADE, front TEXT NOT NULL, back TEXT NOT NULL, position INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS ix_flashcard_libraries_subject ON flashcard_libraries(subject); CREATE INDEX IF NOT EXISTS ix_flashcards_library_id_position ON flashcards(library_id, position); """; await using var connection = CreateConnection(); await connection.OpenAsync(); await connection.ExecuteAsync(sql); } public async Task UpsertLibraryAsync( string filePath, string fileName, string subject, string difficulty, IReadOnlyList cards) { await using var connection = CreateConnection(); await connection.OpenAsync(); await using var transaction = await connection.BeginTransactionAsync(); try { var existingId = await connection.ExecuteScalarAsync( "SELECT id FROM flashcard_libraries WHERE file_path = @FilePath FOR UPDATE;", new { FilePath = filePath }, transaction); long libraryId; if (existingId.HasValue) { libraryId = existingId.Value; await connection.ExecuteAsync( """ UPDATE flashcard_libraries SET file_name = @FileName, subject = @Subject, difficulty = @Difficulty, card_count = @CardCount, updated_at = NOW() WHERE id = @LibraryId; """, new { FileName = fileName, Subject = subject, Difficulty = difficulty, CardCount = cards.Count, LibraryId = libraryId }, transaction); await connection.ExecuteAsync( "DELETE FROM flashcards WHERE library_id = @LibraryId;", new { LibraryId = libraryId }, transaction); } else { libraryId = await connection.ExecuteScalarAsync( """ INSERT INTO flashcard_libraries (file_path, file_name, subject, difficulty, card_count) VALUES (@FilePath, @FileName, @Subject, @Difficulty, @CardCount) RETURNING id; """, new { FilePath = filePath, FileName = fileName, Subject = subject, Difficulty = difficulty, CardCount = cards.Count }, transaction); } if (cards.Count > 0) { await connection.ExecuteAsync( """ INSERT INTO flashcards (library_id, front, back, position) VALUES (@LibraryId, @Front, @Back, @Position); """, cards.Select(card => new { LibraryId = libraryId, card.Front, card.Back, card.Position }), transaction); } await transaction.CommitAsync(); var details = await GetLibraryByIdAsync(libraryId); if (details is null) { throw new InvalidOperationException("Biblioteca gerada nao encontrada apos persistencia."); } return details; } catch { await transaction.RollbackAsync(); throw; } } public async Task> GetLibrariesAsync() { const string sql = """ SELECT id AS Id, file_path AS FilePath, file_name AS FileName, subject AS Subject, difficulty AS Difficulty, card_count AS CardCount, created_at AS CreatedAt, updated_at AS UpdatedAt FROM flashcard_libraries ORDER BY subject, file_name; """; await using var connection = CreateConnection(); await connection.OpenAsync(); var rows = await connection.QueryAsync(sql); return rows.ToList(); } public async Task GetLibraryByIdAsync(long libraryId) { const string librarySql = """ SELECT id AS Id, file_path AS FilePath, file_name AS FileName, subject AS Subject, difficulty AS Difficulty, card_count AS CardCount, created_at AS CreatedAt, updated_at AS UpdatedAt FROM flashcard_libraries WHERE id = @LibraryId; """; const string cardsSql = """ SELECT id AS Id, library_id AS LibraryId, front AS Front, back AS Back, position AS Position, created_at AS CreatedAt FROM flashcards WHERE library_id = @LibraryId ORDER BY position; """; await using var connection = CreateConnection(); await connection.OpenAsync(); var library = await connection.QuerySingleOrDefaultAsync( librarySql, new { LibraryId = libraryId }); if (library is null) { return null; } var cards = await connection.QueryAsync(cardsSql, new { LibraryId = libraryId }); return new FlashcardLibraryDetails { Id = library.Id, FilePath = library.FilePath, FileName = library.FileName, Subject = library.Subject, Difficulty = library.Difficulty, CardCount = library.CardCount, CreatedAt = library.CreatedAt, UpdatedAt = library.UpdatedAt, Cards = cards.ToList() }; } public async Task> GetCardsForLibrariesAsync(IReadOnlyList libraryIds) { if (libraryIds.Count == 0) { return []; } const string sql = """ SELECT id AS Id, library_id AS LibraryId, front AS Front, back AS Back, position AS Position, created_at AS CreatedAt FROM flashcards WHERE library_id = ANY(@LibraryIds) ORDER BY library_id, position; """; await using var connection = CreateConnection(); await connection.OpenAsync(); var cards = await connection.QueryAsync(sql, new { LibraryIds = libraryIds.ToArray() }); return cards.ToList(); } private NpgsqlConnection CreateConnection() { return new NpgsqlConnection(_connectionString); } } }