using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Infrastructure.Grades;

/// <summary>
/// Rebuilds a course/term's denormalized result statistics.  The operation is
/// intentionally idempotent: duplicate RabbitMQ deliveries are safe.
/// </summary>
public sealed class CourseGradeStatisticsRefreshJobProcessor(
    AppDbContext db,
    IAppCache cache,
    ILogger<CourseGradeStatisticsRefreshJobProcessor> logger)
{
    public async Task ProcessAsync(Guid jobId, CancellationToken cancellationToken)
    {
        var job = await db.CourseGradeStatisticsRefreshJobs
            .FirstOrDefaultAsync(x => x.Id == jobId, cancellationToken);
        if (job is null || job.Status == CourseGradeStatisticsRefreshJobStatus.Succeeded)
            return;

        job.Status = CourseGradeStatisticsRefreshJobStatus.Running;
        job.StartedAt = DateTime.UtcNow;
        job.ErrorMessage = null;
        await db.SaveChangesAsync(cancellationToken);

        var sheetData = await db.GradeSheets.AsNoTracking()
            .Where(x => x.Id == job.GradeSheetId)
            .Select(x => new { x.Id, x.TeachingTask!.CourseId, x.TeachingTask.AcademicTermId })
            .FirstOrDefaultAsync(cancellationToken);
        if (sheetData is null)
        {
            job.Status = CourseGradeStatisticsRefreshJobStatus.Succeeded;
            job.CompletedAt = DateTime.UtcNow;
            await db.SaveChangesAsync(cancellationToken);
            return;
        }
        var target = new StatisticsTarget(sheetData.CourseId, sheetData.AcademicTermId);

        try
        {
            // Statistics shown to students are based only on formally published
            // scores. This prevents an unfinished class from exposing data.
            var scores = await db.GradeRecords.AsNoTracking()
                .Where(x => x.TotalScore != null &&
                            x.GradeSheet!.Status == GradeSheetStatus.Published &&
                            x.GradeSheet.TeachingTask!.CourseId == target.CourseId &&
                            x.GradeSheet.TeachingTask.AcademicTermId == target.AcademicTermId)
                .Select(x => new ScoreRow(
                    x.GradeSheetId,
                    x.GradeSheet!.TeachingTaskId,
                    x.TotalScore!.Value,
                    x.Student!.AdministrativeClassId,
                    x.Student.AdministrativeClass!.MajorId,
                    x.Student.AdministrativeClass.Major!.CollegeId))
                .ToListAsync(cancellationToken);

            var now = DateTime.UtcNow;
            var rebuilt = new List<CourseGradeStatistic>();
            AddStatistics(CourseGradeStatisticScope.AdministrativeClass,
                scores.GroupBy(x => x.ClassId), rebuilt, target, now);
            AddStatistics(CourseGradeStatisticScope.Major,
                scores.GroupBy(x => x.MajorId), rebuilt, target, now);
            AddStatistics(CourseGradeStatisticScope.College,
                scores.GroupBy(x => x.CollegeId), rebuilt, target, now);
            AddUniversityStatistic(scores, rebuilt, target, now);

            var rebuiltTeachingTasks = scores
                .GroupBy(x => new { x.GradeSheetId, x.TeachingTaskId })
                .Select(group => CreateTeachingTaskStatistic(
                    group.Key.GradeSheetId,
                    group.Key.TeachingTaskId,
                    group.Select(x => x.Score),
                    target,
                    now))
                .ToList();

            await db.CourseGradeStatistics
                .Where(x => x.CourseId == target.CourseId &&
                            x.AcademicTermId == target.AcademicTermId)
                .ExecuteDeleteAsync(cancellationToken);
            if (rebuilt.Count > 0)
                db.CourseGradeStatistics.AddRange(rebuilt);

            var oldTeachingTaskStatisticIds = await db.TeachingTaskGradeStatistics
                .Where(x => x.CourseId == target.CourseId &&
                            x.AcademicTermId == target.AcademicTermId)
                .Select(x => x.Id)
                .ToListAsync(cancellationToken);
            if (oldTeachingTaskStatisticIds.Count > 0)
            {
                await db.TeachingTaskGradeScoreBands
                    .Where(x => oldTeachingTaskStatisticIds.Contains(
                        x.TeachingTaskGradeStatisticId))
                    .ExecuteDeleteAsync(cancellationToken);
                await db.TeachingTaskGradeStatistics
                    .Where(x => oldTeachingTaskStatisticIds.Contains(x.Id))
                    .ExecuteDeleteAsync(cancellationToken);
            }
            if (rebuiltTeachingTasks.Count > 0)
                db.TeachingTaskGradeStatistics.AddRange(rebuiltTeachingTasks);

            job.Status = CourseGradeStatisticsRefreshJobStatus.Succeeded;
            job.CompletedAt = now;
            await db.SaveChangesAsync(cancellationToken);
            await cache.RemoveByTagAsync(AppCacheTags.CourseGradeStatistics,
                cancellationToken);
        }
        catch (Exception exception)
        {
            job.Status = CourseGradeStatisticsRefreshJobStatus.Failed;
            job.ErrorMessage = exception.GetBaseException().Message[..Math.Min(2000,
                exception.GetBaseException().Message.Length)];
            await db.SaveChangesAsync(CancellationToken.None);
            logger.LogError(exception, "Course grade statistics refresh {JobId} failed.", jobId);
            throw;
        }
    }

    private static void AddStatistics(
        CourseGradeStatisticScope scope,
        IEnumerable<IGrouping<Guid, ScoreRow>> groups,
        ICollection<CourseGradeStatistic> target,
        StatisticsTarget targetInfo,
        DateTime calculatedAt)
    {
        foreach (var group in groups)
            target.Add(Create(scope, group.Key, group.Select(x => x.Score), targetInfo, calculatedAt));
    }

    private static void AddUniversityStatistic(
        IReadOnlyCollection<ScoreRow> scores,
        ICollection<CourseGradeStatistic> target,
        StatisticsTarget targetInfo,
        DateTime calculatedAt)
    {
        if (scores.Count > 0)
            target.Add(Create(CourseGradeStatisticScope.University, null,
                scores.Select(x => x.Score), targetInfo, calculatedAt));
    }

    private static CourseGradeStatistic Create(
        CourseGradeStatisticScope scope,
        Guid? scopeEntityId,
        IEnumerable<decimal> source,
        StatisticsTarget targetInfo,
        DateTime calculatedAt)
    {
        var scores = source.ToArray();
        var passed = scores.Count(x => x >= 60m);
        return new CourseGradeStatistic
        {
            CourseId = targetInfo.CourseId,
            AcademicTermId = targetInfo.AcademicTermId,
            Scope = scope,
            ScopeEntityId = scopeEntityId,
            StudentCount = scores.Length,
            PassedCount = passed,
            Below60Count = scores.Count(x => x < 60m),
            From60To69Count = scores.Count(x => x >= 60m && x < 70m),
            From70To79Count = scores.Count(x => x >= 70m && x < 80m),
            From80To89Count = scores.Count(x => x >= 80m && x < 90m),
            From90To100Count = scores.Count(x => x >= 90m),
            HighestScore = scores.Max(),
            AverageScore = Math.Round(scores.Average(), 1),
            LowestScore = scores.Min(),
            PassRate = Math.Round((decimal)passed / scores.Length * 100m, 2),
            CalculatedAt = calculatedAt
        };
    }

    private static TeachingTaskGradeStatistic CreateTeachingTaskStatistic(
        Guid gradeSheetId,
        Guid teachingTaskId,
        IEnumerable<decimal> source,
        StatisticsTarget targetInfo,
        DateTime calculatedAt)
    {
        var scores = source.OrderBy(x => x).ToArray();
        var passed = scores.Count(x => x >= 60m);
        var excellent = scores.Count(x => x >= 90m);
        var average = scores.Average();
        var middle = scores.Length / 2;
        var median = scores.Length % 2 == 0
            ? (scores[middle - 1] + scores[middle]) / 2m
            : scores[middle];
        var variance = scores.Average(x =>
            (double)((x - average) * (x - average)));

        var statistic = new TeachingTaskGradeStatistic
        {
            GradeSheetId = gradeSheetId,
            TeachingTaskId = teachingTaskId,
            CourseId = targetInfo.CourseId,
            AcademicTermId = targetInfo.AcademicTermId,
            StudentCount = scores.Length,
            PassedCount = passed,
            ExcellentCount = excellent,
            HighestScore = scores.Max(),
            AverageScore = Math.Round(average, 1),
            MedianScore = Math.Round(median, 1),
            LowestScore = scores.Min(),
            StandardDeviation = Math.Round((decimal)Math.Sqrt(variance), 2),
            PassRate = Math.Round((decimal)passed / scores.Length * 100m, 2),
            ExcellentRate = Math.Round((decimal)excellent / scores.Length * 100m, 2),
            CalculatedAt = calculatedAt
        };
        statistic.ScoreBands =
        [
            CreateBand(statistic.Id, "0–59", 0m, 60m,
                scores.Count(x => x < 60m), 0),
            CreateBand(statistic.Id, "60–69", 60m, 70m,
                scores.Count(x => x >= 60m && x < 70m), 1),
            CreateBand(statistic.Id, "70–79", 70m, 80m,
                scores.Count(x => x >= 70m && x < 80m), 2),
            CreateBand(statistic.Id, "80–89", 80m, 90m,
                scores.Count(x => x >= 80m && x < 90m), 3),
            CreateBand(statistic.Id, "90–100", 90m, null,
                scores.Count(x => x >= 90m), 4)
        ];
        return statistic;
    }

    private static TeachingTaskGradeScoreBand CreateBand(
        Guid statisticId,
        string label,
        decimal lowerBound,
        decimal? upperBound,
        int count,
        int sortOrder) => new()
        {
            TeachingTaskGradeStatisticId = statisticId,
            Label = label,
            LowerBound = lowerBound,
            UpperBound = upperBound,
            StudentCount = count,
            SortOrder = sortOrder
        };

    private sealed record ScoreRow(
        Guid GradeSheetId,
        Guid TeachingTaskId,
        decimal Score,
        Guid ClassId,
        Guid MajorId,
        Guid CollegeId);
    private sealed record StatisticsTarget(Guid CourseId, Guid AcademicTermId);
}
