using System.Data.Common;
using Eis.Infrastructure.Caching;
using Eis.Infrastructure.Data;

namespace Eis.Infrastructure.Administration;

internal sealed class AdminExamRepository(
    IRelationalConnectionFactory connectionFactory,
    IApplicationCache cache)
{
    public Task CreateAsync(ManagedExam exam, AdminAuditEntry audit, CancellationToken cancellationToken) =>
        SaveAsync(exam, replaceSubjects: true, isNew: true, audit, cancellationToken);

    public Task UpdateAsync(ManagedExam exam, bool replaceSubjects, AdminAuditEntry audit, CancellationToken cancellationToken) =>
        SaveAsync(exam, replaceSubjects, isNew: false, audit, cancellationToken);

    public async Task ArchiveAsync(ManagedExam exam, 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, new SqlOperation(
                "UPDATE exams SET status = @status, archived_at = @archivedAt, archived_by = @archivedBy WHERE id = @id AND archived_at IS NULL",
                [new("@status", exam.Status), new("@archivedAt", exam.ArchivedAt), new("@archivedBy", exam.ArchivedBy), new("@id", exam.Id)]), cancellationToken);
            await InsertAuditAsync(connection, transaction, audit, cancellationToken);
            await transaction.CommitAsync(cancellationToken);
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
        await cache.InvalidateAsync("public");
    }

    private async Task SaveAsync(
        ManagedExam exam,
        bool replaceSubjects,
        bool isNew,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
        try
        {
            var examOperation = isNew
                ? new SqlOperation(
                    """
                    INSERT INTO exams (
                      id, code, name, description, registration_start, registration_end, exam_start, exam_end,
                      admit_download_start, admit_download_end, location, pass_policy, pass_value, status,
                      archived_at, archived_by, created_at
                    ) VALUES (
                      @id, @code, @name, @description, @registrationStart, @registrationEnd, @examStart, @examEnd,
                      @admitDownloadStart, @admitDownloadEnd, @location, @passPolicy, @passValue, @status,
                      @archivedAt, @archivedBy, @createdAt
                    )
                    """,
                    ExamParameters(exam))
                : new SqlOperation(
                    """
                    UPDATE exams SET code = @code, name = @name, description = @description,
                      registration_start = @registrationStart, registration_end = @registrationEnd,
                      exam_start = @examStart, exam_end = @examEnd,
                      admit_download_start = @admitDownloadStart, admit_download_end = @admitDownloadEnd,
                      location = @location, pass_policy = @passPolicy, pass_value = @passValue, status = @status
                    WHERE id = @id
                    """,
                    ExamParameters(exam));
            await ExecuteAsync(connection, transaction, examOperation, cancellationToken);
            if (replaceSubjects)
            {
                if (!isNew)
                {
                    await ExecuteAsync(connection, transaction, new SqlOperation(
                        "DELETE FROM exam_subjects WHERE exam_id = @examId",
                        [new("@examId", exam.Id)]), cancellationToken);
                }
                foreach (var subject in exam.Subjects)
                {
                    await ExecuteAsync(connection, transaction, new SqlOperation(
                        """
                        INSERT INTO exam_subjects (
                          id, exam_id, name, subject_date, start_time, end_time, fee, full_score,
                          pass_score, pass_rule, pass_value, position
                        ) VALUES (
                          @id, @examId, @name, @date, @start, @end, @fee, @fullScore,
                          @passScore, @passRule, @passValue, @position
                        )
                        """,
                        [
                            new("@id", subject.Id), new("@examId", exam.Id), new("@name", subject.Name),
                            new("@date", subject.Date), new("@start", subject.Start), new("@end", subject.End),
                            new("@fee", subject.Fee), new("@fullScore", subject.FullScore),
                            new("@passScore", subject.PassScore ?? 0),
                            new("@passRule", subject.PassRule == "rank_percent" ? "score_ratio" : subject.PassRule),
                            new("@passValue", subject.PassValue), new("@position", subject.Order)
                        ]), cancellationToken);
                }
            }
            await InsertAuditAsync(connection, transaction, audit, cancellationToken);
            await transaction.CommitAsync(cancellationToken);
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
        await cache.InvalidateAsync("public");
    }

    private static IReadOnlyList<SqlParameterValue> ExamParameters(ManagedExam exam) =>
    [
        new("@id", exam.Id), new("@code", exam.Code), new("@name", exam.Name),
        new("@description", exam.Description), new("@registrationStart", exam.RegistrationStart),
        new("@registrationEnd", exam.RegistrationEnd), new("@examStart", exam.ExamStart),
        new("@examEnd", exam.ExamEnd), new("@admitDownloadStart", exam.AdmitDownloadStart),
        new("@admitDownloadEnd", exam.AdmitDownloadEnd), new("@location", exam.Location),
        new("@passPolicy", exam.PassPolicy), new("@passValue", exam.PassValue), new("@status", exam.Status),
        new("@archivedAt", exam.ArchivedAt), new("@archivedBy", exam.ArchivedBy), new("@createdAt", exam.CreatedAt)
    ];

    private static Task InsertAuditAsync(DbConnection connection, DbTransaction transaction, AdminAuditEntry audit, CancellationToken cancellationToken) =>
        ExecuteAsync(connection, transaction, new SqlOperation(
            """
            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);

    private static async Task ExecuteAsync(
        DbConnection connection,
        DbTransaction transaction,
        SqlOperation operation,
        CancellationToken cancellationToken)
    {
        await using var command = connection.CreateCommand();
        command.Transaction = transaction;
        command.CommandText = operation.Sql;
        foreach (var item in operation.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 SqlOperation(string Sql, IReadOnlyList<SqlParameterValue> Parameters);
    private sealed record SqlParameterValue(string Name, object? Value);
}

internal sealed record ManagedExam(
    string Id,
    string Code,
    string Name,
    string Description,
    string RegistrationStart,
    string RegistrationEnd,
    string ExamStart,
    string ExamEnd,
    string AdmitDownloadStart,
    string AdmitDownloadEnd,
    string Location,
    string PassPolicy,
    double PassValue,
    string Status,
    string? ArchivedAt,
    string? ArchivedBy,
    string CreatedAt,
    IReadOnlyList<ManagedSubject> Subjects);

internal sealed record ManagedSubject(
    string Id,
    string Name,
    string Date,
    string Start,
    string End,
    double Fee,
    double FullScore,
    string PassRule,
    double PassValue,
    double? PassScore,
    int Order);
