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

namespace Eis.Infrastructure.Administration;

internal sealed record ResultWrite(AdminWorkflowResult Result, bool IsNew, AdminAuditEntry Audit);
internal sealed record FeatureScoreWrite(string RegistrationId, double Score, AdminAuditEntry Audit);

internal sealed class AdminResultRepository(IRelationalConnectionFactory connectionFactory)
{
    public Task SaveAsync(ResultWrite entry, CancellationToken cancellationToken) =>
        SaveManyAsync([entry], cancellationToken);

    public Task SaveAsync(
        ResultWrite entry,
        AdminResultCalculation calculation,
        CancellationToken cancellationToken) =>
        SaveManyAsync([entry], [calculation], cancellationToken);

    public async Task SaveManyAsync(IReadOnlyList<ResultWrite> entries, CancellationToken cancellationToken)
        => await SaveManyAsync(entries, [], cancellationToken);

    public async Task SaveManyAsync(
        IReadOnlyList<ResultWrite> entries,
        IReadOnlyList<AdminResultCalculation> calculations,
        CancellationToken cancellationToken)
    {
        await TransactionAsync(async (connection, transaction) =>
        {
            foreach (var entry in entries)
            {
                var result = entry.Result;
                await ExecuteAsync(
                    connection,
                    transaction,
                    entry.IsNew
                        ? """
                          INSERT INTO results (
                            id, registration_id, subject_id, score, grade, published, updated_at, published_at
                          ) VALUES (
                            @id, @registrationId, @subjectId, @score, @grade, @published, @updatedAt, @publishedAt
                          )
                          """
                        : """
                          UPDATE results SET score = @score, grade = @grade, published = @published,
                            updated_at = @updatedAt, published_at = @publishedAt WHERE id = @id
                          """,
                    [
                        new("@id", result.Id),
                        new("@registrationId", result.RegistrationId),
                        new("@subjectId", result.SubjectId),
                        new("@score", result.Score),
                        new("@grade", result.Grade),
                        new("@published", result.Published ? 1 : 0),
                        new("@updatedAt", result.UpdatedAt),
                        new("@publishedAt", result.PublishedAt)
                    ],
                    cancellationToken);
                await InsertAuditAsync(connection, transaction, entry.Audit, cancellationToken);
            }
            foreach (var calculation in calculations)
            {
                await ReplaceStatisticsAsync(connection, transaction, calculation, cancellationToken);
            }
        }, cancellationToken);
    }

    public async Task ReplaceStatisticsAsync(
        IReadOnlyList<AdminResultCalculation> calculations,
        CancellationToken cancellationToken)
    {
        await TransactionAsync(async (connection, transaction) =>
        {
            foreach (var calculation in calculations)
            {
                await ReplaceStatisticsAsync(connection, transaction, calculation, cancellationToken);
            }
        }, cancellationToken);
    }

    public async Task<bool> NeedsStatisticsBackfillAsync(CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var command = connection.CreateCommand();
        command.CommandText =
            """
            SELECT
              (SELECT COUNT(*) FROM results result
                 LEFT JOIN result_statistics statistic ON statistic.result_id = result.id
                WHERE statistic.result_id IS NULL) +
              (SELECT COUNT(*) FROM registrations registration
                 LEFT JOIN registration_result_summaries summary ON summary.registration_id = registration.id
                WHERE registration.status = 'approved' AND summary.registration_id IS NULL)
            """;
        return Convert.ToInt64(await command.ExecuteScalarAsync(cancellationToken)) > 0;
    }

    public async Task SaveFeatureScoresAsync(
        IReadOnlyList<FeatureScoreWrite> entries,
        CancellationToken cancellationToken)
    {
        await TransactionAsync(async (connection, transaction) =>
        {
            foreach (var entry in entries)
            {
                await ExecuteAsync(
                    connection,
                    transaction,
                    "UPDATE registrations SET feature_score = @score WHERE id = @id",
                    [new("@score", entry.Score), new("@id", entry.RegistrationId)],
                    cancellationToken);
                await InsertAuditAsync(connection, transaction, entry.Audit, cancellationToken);
            }
        }, cancellationToken);
    }

    public async Task ReviewAppealAsync(
        AccountWorkflowInstance instance,
        AccountWorkflowAction action,
        AdminWorkflowResult? result,
        AdminResultCalculation? calculation,
        AdminAuditEntry audit,
        CancellationToken cancellationToken)
    {
        await TransactionAsync(async (connection, transaction) =>
        {
            await ExecuteAsync(
                connection,
                transaction,
                """
                UPDATE workflow_instances SET status = @status, current_step = @currentStep,
                  assignee_id = @assigneeId, completed_at = @completedAt WHERE id = @id
                """,
                [
                    new("@status", instance.Status),
                    new("@currentStep", instance.CurrentStep),
                    new("@assigneeId", instance.AssigneeId),
                    new("@completedAt", instance.CompletedAt),
                    new("@id", instance.Id)
                ],
                cancellationToken);
            await ExecuteAsync(
                connection,
                transaction,
                """
                INSERT INTO workflow_actions (
                  id, instance_id, actor_id, action, note, from_assignee_id, to_assignee_id, created_at
                ) VALUES (
                  @id, @instanceId, @actorId, @action, @note, @fromAssigneeId, @toAssigneeId, @createdAt
                )
                """,
                [
                    new("@id", action.Id),
                    new("@instanceId", action.InstanceId),
                    new("@actorId", action.ActorId),
                    new("@action", action.Action),
                    new("@note", action.Note),
                    new("@fromAssigneeId", action.FromAssigneeId),
                    new("@toAssigneeId", action.ToAssigneeId),
                    new("@createdAt", action.CreatedAt)
                ],
                cancellationToken);
            if (result is not null)
            {
                await ExecuteAsync(
                    connection,
                    transaction,
                    """
                    UPDATE results SET score = @score, grade = @grade, updated_at = @updatedAt
                    WHERE id = @id
                    """,
                    [
                        new("@score", result.Score),
                        new("@grade", result.Grade),
                        new("@updatedAt", result.UpdatedAt),
                        new("@id", result.Id)
                    ],
                    cancellationToken);
            }
            if (calculation is not null)
            {
                await ReplaceStatisticsAsync(connection, transaction, calculation, cancellationToken);
            }
            await InsertAuditAsync(connection, transaction, audit, cancellationToken);
        }, cancellationToken);
    }

    private static async Task ReplaceStatisticsAsync(
        DbConnection connection,
        DbTransaction transaction,
        AdminResultCalculation calculation,
        CancellationToken cancellationToken)
    {
        await ExecuteAsync(
            connection,
            transaction,
            """
            DELETE FROM result_statistics
            WHERE result_id IN (
              SELECT result.id FROM results result
              JOIN registrations registration ON registration.id = result.registration_id
              WHERE registration.exam_id = @examId
            )
            """,
            [new("@examId", calculation.ExamId)],
            cancellationToken);
        await ExecuteAsync(
            connection,
            transaction,
            "DELETE FROM registration_result_summaries WHERE exam_id = @examId",
            [new("@examId", calculation.ExamId)],
            cancellationToken);
        foreach (var metric in calculation.Metrics)
        {
            await ExecuteAsync(
                connection,
                transaction,
                """
                INSERT INTO result_statistics (
                  result_id, subject_rank, cohort_size, rank_percent, grade, qualified,
                  pass_score, cutoff_rank, calculated_at
                ) VALUES (
                  @resultId, @rank, @cohortSize, @rankPercent, @grade, @qualified,
                  @passScore, @cutoffRank, @calculatedAt
                )
                """,
                [
                    new("@resultId", metric.ResultId),
                    new("@rank", metric.Rank),
                    new("@cohortSize", metric.CohortSize),
                    new("@rankPercent", metric.RankPercent),
                    new("@grade", metric.Grade),
                    new("@qualified", metric.Qualified is null ? null : metric.Qualified.Value ? 1 : 0),
                    new("@passScore", metric.PassScore),
                    new("@cutoffRank", metric.CutoffRank),
                    new("@calculatedAt", metric.CalculatedAt)
                ],
                cancellationToken);
            await ExecuteAsync(
                connection,
                transaction,
                "UPDATE results SET grade = @grade WHERE id = @resultId",
                [new("@grade", metric.Grade), new("@resultId", metric.ResultId)],
                cancellationToken);
        }
        foreach (var summary in calculation.Summaries)
        {
            await ExecuteAsync(
                connection,
                transaction,
                """
                INSERT INTO registration_result_summaries (
                  registration_id, exam_id, published_subject_count, subject_count, complete,
                  total_score, full_score, score_ratio, total_rank, cohort_size, rank_percent,
                  qualified, cutoff_rank, calculated_at
                ) VALUES (
                  @registrationId, @examId, @publishedSubjectCount, @subjectCount, @complete,
                  @totalScore, @fullScore, @scoreRatio, @rank, @cohortSize, @rankPercent,
                  @qualified, @cutoffRank, @calculatedAt
                )
                """,
                [
                    new("@registrationId", summary.RegistrationId),
                    new("@examId", summary.ExamId),
                    new("@publishedSubjectCount", summary.PublishedSubjectCount),
                    new("@subjectCount", summary.SubjectCount),
                    new("@complete", summary.Complete ? 1 : 0),
                    new("@totalScore", summary.TotalScore),
                    new("@fullScore", summary.FullScore),
                    new("@scoreRatio", summary.ScoreRatio),
                    new("@rank", summary.Rank),
                    new("@cohortSize", summary.CohortSize),
                    new("@rankPercent", summary.RankPercent),
                    new("@qualified", summary.Qualified is null ? null : summary.Qualified.Value ? 1 : 0),
                    new("@cutoffRank", summary.CutoffRank),
                    new("@calculatedAt", summary.CalculatedAt)
                ],
                cancellationToken);
        }
    }

    private async Task TransactionAsync(
        Func<DbConnection, DbTransaction, Task> operation,
        CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var transaction = await connection.BeginTransactionAsync(cancellationToken);
        try
        {
            await operation(connection, transaction);
            await transaction.CommitAsync(cancellationToken);
        }
        catch
        {
            await transaction.RollbackAsync(cancellationToken);
            throw;
        }
    }

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

    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);
}
