using System.Data.Common;
using System.Text.Json;
using Eis.Infrastructure.Data;

namespace Eis.Infrastructure.Administration;

internal sealed class AdminArrangementRepository(IRelationalConnectionFactory connectionFactory)
{
    public async Task SaveAsync(
        StoredArrangementPlan plan,
        IReadOnlyList<ArrangementCard> cards,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
        try
        {
            await ExecuteAsync(
                connection,
                transaction,
                "DELETE FROM exam_arrangement_plans WHERE exam_id = @examId",
                [new("@examId", plan.ExamId)],
                cancellationToken);
            await ExecuteAsync(
                connection,
                transaction,
                """
                INSERT INTO exam_arrangement_plans (
                  id, exam_id, number_rule_id, mixing_scope, random_seed, candidate_count, center_count,
                  subject_assignment_count, subject_combination_count, same_school_center_rate,
                  warnings_json, generated_by, generated_at
                ) VALUES (
                  @id, @examId, @numberRuleId, @mixingScope, @randomSeed, @candidateCount, @centerCount,
                  @subjectAssignmentCount, @subjectCombinationCount, @sameSchoolCenterRate,
                  @warningsJson, @generatedBy, @generatedAt
                )
                """,
                [
                    new("@id", plan.Id),
                    new("@examId", plan.ExamId),
                    new("@numberRuleId", plan.NumberRuleId),
                    new("@mixingScope", plan.MixingScope),
                    new("@randomSeed", plan.RandomSeed),
                    new("@candidateCount", plan.CandidateCount),
                    new("@centerCount", plan.CenterCount),
                    new("@subjectAssignmentCount", plan.SubjectAssignmentCount),
                    new("@subjectCombinationCount", plan.SubjectCombinationCount),
                    new("@sameSchoolCenterRate", plan.SameSchoolCenterRate),
                    new("@warningsJson", JsonSerializer.Serialize(plan.Warnings)),
                    new("@generatedBy", plan.GeneratedBy),
                    new("@generatedAt", plan.GeneratedAt)
                ],
                cancellationToken);
            foreach (var card in cards)
            {
                await ExecuteAsync(
                    connection,
                    transaction,
                    """
                    INSERT INTO admit_cards (
                      registration_id, plan_id, card_number, center_id, test_center, center_code,
                      center_address, generated_at
                    ) VALUES (
                      @registrationId, @planId, @cardNumber, @centerId, @testCenter, @centerCode,
                      @centerAddress, @generatedAt
                    )
                    """,
                    [
                        new("@registrationId", card.RegistrationId),
                        new("@planId", plan.Id),
                        new("@cardNumber", card.Number),
                        new("@centerId", card.CenterId),
                        new("@testCenter", card.TestCenter),
                        new("@centerCode", card.CenterCode),
                        new("@centerAddress", card.CenterAddress),
                        new("@generatedAt", card.GeneratedAt)
                    ],
                    cancellationToken);
                foreach (var assignment in card.Assignments)
                {
                    await ExecuteAsync(
                        connection,
                        transaction,
                        """
                        INSERT INTO admit_card_subjects (
                          registration_id, subject_id, room_id, room, room_code, exam_room_code,
                          building, floor, seat, subject_signature
                        ) VALUES (
                          @registrationId, @subjectId, @roomId, @room, @roomCode, @examRoomCode,
                          @building, @floor, @seat, @subjectSignature
                        )
                        """,
                        [
                            new("@registrationId", card.RegistrationId),
                            new("@subjectId", assignment.SubjectId),
                            new("@roomId", assignment.RoomId),
                            new("@room", assignment.RoomName),
                            new("@roomCode", assignment.RoomCode),
                            new("@examRoomCode", assignment.ExamRoomCode),
                            new("@building", assignment.Building),
                            new("@floor", assignment.Floor),
                            new("@seat", assignment.Seat),
                            new("@subjectSignature", assignment.SubjectSignature)
                        ],
                        cancellationToken);
                }
            }
            await ExecuteAsync(
                connection,
                transaction,
                """
                INSERT INTO audit_logs (id, actor_id, action, detail, created_at)
                VALUES (@id, @actorId, @action, @detail, @createdAt)
                """,
                [
                    new("@id", audit.Id),
                    new("@actorId", audit.ActorId),
                    new("@action", audit.Action),
                    new("@detail", audit.Detail),
                    new("@createdAt", audit.CreatedAt)
                ],
                cancellationToken);
            await transaction.CommitAsync(cancellationToken);
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }

    private static async Task ExecuteAsync(
        DbConnection connection,
        DbTransaction transaction,
        string sql,
        IReadOnlyList<SqlParameterValue> parameters,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.Transaction = transaction;
        command.CommandText = sql;
        foreach (var item in parameters)
        {
            var parameter = command.CreateParameter();
            parameter.ParameterName = item.Name;
            parameter.Value = item.Value ?? DBNull.Value;
            command.Parameters.Add(parameter);
        }
        await command.ExecuteNonQueryAsync(cancellationToken);
    }

    private sealed record SqlParameterValue(string Name, object? Value);
}
