using Dapper;
using Mindforge.API.Exceptions;
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,
                    correct_count INTEGER NOT NULL DEFAULT 0,
                    incorrect_count INTEGER NOT NULL DEFAULT 0,
                    last_reviewed_at TIMESTAMPTZ NULL,
                    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
                );

                ALTER TABLE flashcards
                    ADD COLUMN IF NOT EXISTS correct_count INTEGER NOT NULL DEFAULT 0;

                ALTER TABLE flashcards
                    ADD COLUMN IF NOT EXISTS incorrect_count INTEGER NOT NULL DEFAULT 0;

                ALTER TABLE flashcards
                    ADD COLUMN IF NOT EXISTS last_reviewed_at TIMESTAMPTZ NULL;

                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);
                CREATE INDEX IF NOT EXISTS ix_flashcards_last_reviewed_at ON flashcards(last_reviewed_at);
                """;

            await using var connection = CreateConnection();
            await connection.OpenAsync();
            await connection.ExecuteAsync(sql);
        }

        public async Task<FlashcardLibraryDetails> UpsertLibraryAsync(
            string filePath,
            string fileName,
            string subject,
            string difficulty,
            IReadOnlyList<FlashcardDraftCard> cards)
        {
            await using var connection = CreateConnection();
            await connection.OpenAsync();
            await using var transaction = await connection.BeginTransactionAsync();

            try
            {
                var existingId = await connection.ExecuteScalarAsync<long?>(
                    "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<long>(
                        """
                        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<IReadOnlyList<FlashcardLibrarySummary>> 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<FlashcardLibrarySummary>(sql);
            return rows.ToList();
        }

        public async Task<FlashcardLibraryDetails?> 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,
                    correct_count AS CorrectCount,
                    incorrect_count AS IncorrectCount,
                    last_reviewed_at AS LastReviewedAt,
                    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<FlashcardLibrarySummary>(
                librarySql,
                new { LibraryId = libraryId });

            if (library is null)
            {
                return null;
            }

            var cards = await connection.QueryAsync<FlashcardCard>(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<IReadOnlyList<FlashcardCard>> GetCardsForLibrariesAsync(IReadOnlyList<long> 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,
                    correct_count AS CorrectCount,
                    incorrect_count AS IncorrectCount,
                    last_reviewed_at AS LastReviewedAt,
                    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<FlashcardCard>(sql, new { LibraryIds = libraryIds.ToArray() });
            return cards.ToList();
        }

        public async Task RecordReviewAnswerAsync(long cardId, bool correct)
        {
            var sql = correct
                ? "UPDATE flashcards SET correct_count = correct_count + 1, last_reviewed_at = NOW() WHERE id = @CardId;"
                : "UPDATE flashcards SET incorrect_count = incorrect_count + 1, last_reviewed_at = NOW() WHERE id = @CardId;";

            await using var connection = CreateConnection();
            await connection.OpenAsync();

            var affectedRows = await connection.ExecuteAsync(sql, new { CardId = cardId });
            if (affectedRows == 0)
            {
                throw new UserException("Card de revisao nao encontrado para registrar resposta.");
            }
        }

        public async Task<IReadOnlyList<FlashcardCardWithLibrary>> GetAllCardsWithLibraryAsync()
        {
            const string sql = """
                SELECT
                    f.id AS Id,
                    f.library_id AS LibraryId,
                    f.front AS Front,
                    f.back AS Back,
                    f.position AS Position,
                    f.correct_count AS CorrectCount,
                    f.incorrect_count AS IncorrectCount,
                    f.last_reviewed_at AS LastReviewedAt,
                    f.created_at AS CreatedAt,
                    l.file_path AS FilePath,
                    l.file_name AS FileName,
                    l.subject AS Subject
                FROM flashcards f
                INNER JOIN flashcard_libraries l ON l.id = f.library_id
                ORDER BY l.subject, l.file_name, f.position;
                """;

            await using var connection = CreateConnection();
            await connection.OpenAsync();
            var cards = await connection.QueryAsync<FlashcardCardWithLibrary>(sql);
            return cards.ToList();
        }

        private NpgsqlConnection CreateConnection()
        {
            return new NpgsqlConnection(_connectionString);
        }
    }
}
