using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Infrastructure.Grades;

public static class ExperimentGradeAggregationService
{
    public static async Task RefreshTeachingTaskAsync(
        AppDbContext db,
        Guid teachingTaskId,
        CancellationToken cancellationToken)
    {
        var sheets = await LoadPublishedSheetsAsync(
            db,
            teachingTaskId,
            cancellationToken);
        var existing = await db.ExperimentCourseGrades
            .Where(x => x.TeachingTaskId == teachingTaskId)
            .ToDictionaryAsync(x => x.StudentId, cancellationToken);
        var studentIds = sheets
            .SelectMany(x => x.Scores.Select(score => score.StudentId))
            .Distinct()
            .ToArray();
        var refreshedAt = DateTime.UtcNow;
        var totalWeight = sheets.Sum(x => x.ContributionWeight);

        foreach (var studentId in studentIds)
        {
            var score = CalculateStudentScore(sheets, studentId);
            if (!existing.Remove(studentId, out var aggregate))
            {
                aggregate = new Domain.Academic.ExperimentCourseGrade
                {
                    TeachingTaskId = teachingTaskId,
                    StudentId = studentId
                };
                db.ExperimentCourseGrades.Add(aggregate);
            }
            aggregate.WeightedAverageScore = score;
            aggregate.TotalWeight = totalWeight;
            aggregate.PublishedProjectCount = sheets.Count;
            aggregate.RefreshedAt = refreshedAt;
        }

        db.ExperimentCourseGrades.RemoveRange(existing.Values);
        await db.SaveChangesAsync(cancellationToken);
    }

    public static async Task<ExperimentGradeAggregateResult> CalculateAsync(
        AppDbContext db,
        Guid teachingTaskId,
        IReadOnlyCollection<Guid> studentIds,
        CancellationToken cancellationToken)
    {
        var sheets = await LoadPublishedSheetsAsync(
            db,
            teachingTaskId,
            cancellationToken);
        var requestedStudentIds = studentIds.Distinct().ToArray();
        var persistedScores = await db.ExperimentCourseGrades.AsNoTracking()
            .Where(x => x.TeachingTaskId == teachingTaskId)
            .WhereIn(requestedStudentIds, x => x.StudentId)
            .ToDictionaryAsync(
                x => x.StudentId,
                x => x.WeightedAverageScore,
                cancellationToken);
        var scores = requestedStudentIds.ToDictionary(
            studentId => studentId,
            studentId => persistedScores.GetValueOrDefault(studentId));

        return new ExperimentGradeAggregateResult(
            sheets.Count,
            sheets.Select(x => new ExperimentGradeAggregateProject(
                x.Id,
                x.Code,
                x.Name,
                x.ContributionWeight)).ToList(),
            scores);
    }

    private static Task<List<PublishedExperimentSheet>> LoadPublishedSheetsAsync(
        AppDbContext db,
        Guid teachingTaskId,
        CancellationToken cancellationToken) =>
        db.ExperimentGradeSheets.AsNoTracking()
            .Where(x =>
                x.Status ==
                    Domain.Academic.ExperimentGradeSheetStatus.Published &&
                x.ExperimentProject!.TeachingTaskId == teachingTaskId)
            .OrderBy(x => x.ExperimentProject!.Code)
            .Select(x => new PublishedExperimentSheet(
                x.Id,
                x.ExperimentProject!.Code,
                x.ExperimentProject.Name,
                x.ContributionWeight,
                x.Records.Select(record => new PublishedExperimentScore(
                    record.StudentId,
                    record.TotalScore)).ToList()))
            .AsSplitQuery()
            .ToListAsync(cancellationToken);

    private static decimal? CalculateStudentScore(
        IReadOnlyCollection<PublishedExperimentSheet> sheets,
        Guid studentId)
    {
        decimal weightedTotal = 0;
        decimal totalWeight = 0;
        if (sheets.Count == 0) return null;
        foreach (var sheet in sheets)
        {
            var score = sheet.Scores.FirstOrDefault(x =>
                x.StudentId == studentId);
            if (score?.TotalScore is not decimal totalScore) return null;
            weightedTotal += totalScore * sheet.ContributionWeight;
            totalWeight += sheet.ContributionWeight;
        }
        return totalWeight > 0
            ? Math.Round(
                weightedTotal / totalWeight,
                1,
                MidpointRounding.AwayFromZero)
            : null;
    }

    private sealed record PublishedExperimentSheet(
        Guid Id,
        string Code,
        string Name,
        decimal ContributionWeight,
        List<PublishedExperimentScore> Scores);

    private sealed record PublishedExperimentScore(
        Guid StudentId,
        decimal? TotalScore);
}

public sealed record ExperimentGradeAggregateResult(
    int PublishedProjectCount,
    IReadOnlyList<ExperimentGradeAggregateProject> Projects,
    IReadOnlyDictionary<Guid, decimal?> Scores);

public sealed record ExperimentGradeAggregateProject(
    Guid Id,
    string Code,
    string Name,
    decimal ContributionWeight);
