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

namespace Eis.Infrastructure.Administration;

internal sealed record AdminWorkflowResult(
    string Id,
    string RegistrationId,
    string SubjectId,
    double Score,
    string Grade,
    bool Published,
    string? UpdatedAt,
    string? PublishedAt)
{
    public int? Rank { get; init; }
    public int CohortSize { get; init; }
    public double? RankPercent { get; init; }
    public bool? Qualified { get; init; }
    public double? PassScore { get; init; }
    public int? CutoffRank { get; init; }
}

internal sealed class AdminWorkflowResultSnapshotLoader(IRelationalConnectionFactory connectionFactory)
{
    public async Task<IReadOnlyList<AdminWorkflowResult>> LoadAsync(CancellationToken cancellationToken)
        => await LoadAsync(null, null, cancellationToken);

    public async Task<IReadOnlyList<AdminWorkflowResult>> LoadForExamAsync(
        string examId,
        CancellationToken cancellationToken,
        string? subjectId = null) =>
        await LoadAsync(examId, subjectId, cancellationToken);

    public async Task<IReadOnlyList<AdminRegistrationResultSummary>> LoadSummariesForExamAsync(
        string examId,
        CancellationToken cancellationToken)
        => await LoadSummariesAsync(examId, cancellationToken);

    public async Task<IReadOnlyList<AdminRegistrationResultSummary>> LoadSummariesAsync(
        CancellationToken cancellationToken)
        => await LoadSummariesAsync(null, cancellationToken);

    private async Task<IReadOnlyList<AdminRegistrationResultSummary>> LoadSummariesAsync(
        string? examId,
        CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var command = connection.CreateCommand();
        command.CommandText =
            """
            SELECT 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
            FROM registration_result_summaries
            WHERE (@examId IS NULL OR exam_id = @examId) ORDER BY registration_id
            """;
        AddParameter(command, "@examId", examId);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        var output = new List<AdminRegistrationResultSummary>();
        while (await reader.ReadAsync(cancellationToken))
        {
            output.Add(new(
                Text(reader, "registration_id"),
                Text(reader, "exam_id"),
                Integer(reader, "published_subject_count") ?? 0,
                Integer(reader, "subject_count") ?? 0,
                Boolean(reader, "complete"),
                Number(reader, "total_score"),
                Number(reader, "full_score"),
                Number(reader, "score_ratio"),
                Integer(reader, "total_rank"),
                Integer(reader, "cohort_size") ?? 0,
                NullableNumber(reader, "rank_percent"),
                NullableBoolean(reader, "qualified"),
                Integer(reader, "cutoff_rank"),
                Text(reader, "calculated_at")));
        }
        return output;
    }

    private async Task<IReadOnlyList<AdminWorkflowResult>> LoadAsync(
        string? examId,
        string? subjectId,
        CancellationToken cancellationToken)
    {
        await using var connection = await connectionFactory.OpenAsync(cancellationToken);
        await using var command = connection.CreateCommand();
        command.CommandText =
            """
            SELECT result.id, result.registration_id, result.subject_id, result.score,
              COALESCE(statistic.grade, result.grade) AS grade, result.published,
              result.updated_at, result.published_at, statistic.subject_rank,
              statistic.cohort_size, statistic.rank_percent, statistic.qualified,
              statistic.pass_score, statistic.cutoff_rank
            FROM results result
            JOIN registrations registration ON registration.id = result.registration_id
            LEFT JOIN result_statistics statistic ON statistic.result_id = result.id
            WHERE (@examId IS NULL OR registration.exam_id = @examId)
              AND (@subjectId IS NULL OR result.subject_id = @subjectId)
            ORDER BY result.id
            """;
        AddParameter(command, "@examId", examId);
        AddParameter(command, "@subjectId", subjectId);
        await using var reader = await command.ExecuteReaderAsync(cancellationToken);
        var output = new List<AdminWorkflowResult>();
        while (await reader.ReadAsync(cancellationToken))
        {
            output.Add(new AdminWorkflowResult(
                Text(reader, "id"),
                Text(reader, "registration_id"),
                Text(reader, "subject_id"),
                Number(reader, "score"),
                Text(reader, "grade"),
                Boolean(reader, "published"),
                Optional(reader, "updated_at"),
                Optional(reader, "published_at"))
            {
                Rank = Integer(reader, "subject_rank"),
                CohortSize = Integer(reader, "cohort_size") ?? 0,
                RankPercent = NullableNumber(reader, "rank_percent"),
                Qualified = NullableBoolean(reader, "qualified"),
                PassScore = NullableNumber(reader, "pass_score"),
                CutoffRank = Integer(reader, "cutoff_rank")
            });
        }
        return output;
    }

    private static void AddParameter(DbCommand command, string name, object? value)
    {
        var parameter = command.CreateParameter();
        parameter.ParameterName = name;
        parameter.Value = value ?? DBNull.Value;
        command.Parameters.Add(parameter);
    }

    private static string Text(DbDataReader reader, string name) =>
        Convert.ToString(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture) ?? "";

    private static double Number(DbDataReader reader, string name) =>
        Convert.ToDouble(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture);

    private static double? NullableNumber(DbDataReader reader, string name)
    {
        var ordinal = reader.GetOrdinal(name);
        return reader.IsDBNull(ordinal)
            ? null
            : Convert.ToDouble(reader.GetValue(ordinal), CultureInfo.InvariantCulture);
    }

    private static int? Integer(DbDataReader reader, string name)
    {
        var ordinal = reader.GetOrdinal(name);
        return reader.IsDBNull(ordinal)
            ? null
            : Convert.ToInt32(reader.GetValue(ordinal), CultureInfo.InvariantCulture);
    }

    private static bool Boolean(DbDataReader reader, string name) =>
        Convert.ToInt64(reader.GetValue(reader.GetOrdinal(name)), CultureInfo.InvariantCulture) != 0;

    private static bool? NullableBoolean(DbDataReader reader, string name)
    {
        var ordinal = reader.GetOrdinal(name);
        return reader.IsDBNull(ordinal)
            ? null
            : Convert.ToInt64(reader.GetValue(ordinal), CultureInfo.InvariantCulture) != 0;
    }

    private static string? Optional(DbDataReader reader, string name)
    {
        var ordinal = reader.GetOrdinal(name);
        return reader.IsDBNull(ordinal)
            ? null
            : Convert.ToString(reader.GetValue(ordinal), CultureInfo.InvariantCulture);
    }
}
