using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Graduation;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Controllers;

[ApiController]
[Authorize(Roles = SystemRoles.Student)]
[Route("api/student/academic-planning")]
public sealed class AcademicPlanningController(
    AppDbContext db,
    ICurrentUserDataScope currentUserDataScope) : ControllerBase
{
    [HttpGet]
    public async Task<ActionResult> Get(CancellationToken cancellationToken)
    {
        var loaded = await LoadAsync(cancellationToken);
        if (loaded.Error is not null) return loaded.Error;
        var context = loaded.Context!;

        var completion = CurriculumCompletionRules.Evaluate(
            context.Plan.Modules,
            context.PassedCourseIds);
        var planCompletedCredits = context.Courses
            .Where(x => context.PassedCourseIds.Contains(x.CourseId))
            .Sum(x => x.Credits);
        var suggestionIds = AcademicPlanningRules.SuggestNextSemester(
            context.CourseSnapshots,
            context.PassedCourseIds,
            context.InProgressCourseIds,
            context.NextSemester);
        var suggestionIdSet = suggestionIds.ToHashSet();
        var latestAudit = await db.GraduationAuditResults.AsNoTracking()
            .Where(x =>
                x.StudentId == context.Student.Id &&
                x.GraduationAuditBatch!.Status ==
                    GraduationAuditBatchStatus.Published)
            .OrderByDescending(x => x.GraduationAuditBatch!.GraduationYear)
            .ThenByDescending(x => x.GraduationAuditBatch!.PublishedAt)
            .Select(x => new
            {
                BatchName = x.GraduationAuditBatch!.Name,
                x.GraduationAuditBatch.GraduationYear,
                x.RequiredCredits,
                x.EarnedCredits,
                x.RequiredCourseCount,
                x.PassedRequiredCourseCount,
                x.FailedCourseCount,
                x.MissingCourseNames,
                x.Conclusion,
                x.IsOverridden,
                x.ReviewComment,
                x.GraduationAuditBatch.PublishedAt
            })
            .FirstOrDefaultAsync(cancellationToken);

        var endSemester = Math.Max(
            context.Plan.Major!.SchoolingYears * 2,
            context.NextSemester + 3);
        return Ok(new
        {
            Student = new
            {
                context.Student.Id,
                context.Student.StudentNumber,
                context.Student.Name,
                context.Student.EnrollmentYear,
                context.Student.Status,
                ClassName = context.Student.AdministrativeClass!.Name,
                MajorName = context.Student.AdministrativeClass.Major!.Name,
                CollegeName =
                    context.Student.AdministrativeClass.Major.College!.Name
            },
            Plan = new
            {
                context.Plan.Id,
                context.Plan.Name,
                context.Plan.Version,
                context.Plan.TotalCredits,
                SchoolingYears = context.Plan.Major.SchoolingYears,
                CurrentSemester = context.CurrentSemester,
                NextSemester = context.NextSemester,
                StandardGraduationSemester =
                    context.Plan.Major.SchoolingYears * 2
            },
            Baseline = new
            {
                EarnedCredits = context.EarnedCredits,
                PlanCompletedCredits = planCompletedCredits,
                CreditGap = Math.Max(
                    context.Plan.TotalCredits - context.EarnedCredits,
                    0),
                CompletionRate = context.Plan.TotalCredits <= 0
                    ? 0
                    : Math.Min(100, Math.Round(
                        planCompletedCredits / context.Plan.TotalCredits * 100,
                        1)),
                completion.RequirementCount,
                completion.PassedRequirementCount,
                completion.MissingRequirements,
                InProgressCredits = context.Courses
                    .Where(x => context.InProgressCourseIds.Contains(x.CourseId))
                    .Sum(x => x.Credits)
            },
            Terms = Enumerable.Range(
                    context.NextSemester,
                    endSemester - context.NextSemester + 1)
                .Select(semester => new
                {
                    Semester = semester,
                    Label = FormatSemester(
                        context.Student.EnrollmentYear,
                        semester),
                    IsBeyondStandard =
                        semester > context.Plan.Major.SchoolingYears * 2
                }),
            Courses = context.Courses
                .OrderBy(x => x.RecommendedSemester)
                .ThenBy(x => x.CourseCode)
                .Select(course => new
                {
                    course.CourseId,
                    course.CourseCode,
                    course.CourseName,
                    course.Credits,
                    course.RecommendedSemester,
                    course.Type,
                    course.ModuleCode,
                    course.ModuleName,
                    Status = context.StatusByCourse[course.CourseId],
                    IsSuggested = suggestionIdSet.Contains(course.CourseId),
                    Prerequisites = course.Prerequisites.Select(item => new
                    {
                        item.CourseId,
                        item.CourseCode,
                        item.CourseName,
                        IsCompleted =
                            context.PassedCourseIds.Contains(item.CourseId),
                        IsInProgress =
                            context.InProgressCourseIds.Contains(item.CourseId)
                    })
                }),
            NextSemesterSuggestion = suggestionIds.Select(courseId =>
            {
                var course = context.CourseById[courseId];
                return new
                {
                    course.CourseId,
                    course.CourseCode,
                    course.CourseName,
                    course.Credits,
                    course.Type,
                    Reason = course.Type == CurriculumCourseType.Required &&
                             course.RecommendedSemester <= context.NextSemester
                        ? "计划学期已到，且先修条件已满足"
                        : course.Type == CurriculumCourseType.Required
                            ? "必修课程，按培养方案顺序推进"
                            : "用于补足培养模块与总学分"
                };
            }),
            LatestGraduationAudit = latestAudit,
            Assumptions = new[]
            {
                "模拟课程按顺利通过计算，不会写入成绩或正式选课。",
                $"预计毕业学期按每学期最多 {AcademicPlanningRules.RecommendedSemesterCreditLimit:0} 学分估算。",
                "当前在读课程按本学期顺利完成计入预测。"
            }
        });
    }

    [HttpPost("simulate")]
    public async Task<ActionResult> Simulate(
        AcademicPlanningRequest request,
        CancellationToken cancellationToken)
    {
        var loaded = await LoadAsync(cancellationToken);
        if (loaded.Error is not null) return loaded.Error;
        var context = loaded.Context!;

        var selections = request.Terms
            .SelectMany(term => term.CourseIds.Select(courseId => new
            {
                term.Semester,
                CourseId = courseId
            }))
            .ToList();
        var duplicate = selections
            .GroupBy(x => x.CourseId)
            .FirstOrDefault(group => group.Count() > 1);
        if (duplicate is not null)
            return ValidationProblem("同一门课程不能安排在多个学期。");
        if (request.Terms.Any(x =>
                x.Semester < context.NextSemester ||
                x.Semester > context.NextSemester + 12))
            return ValidationProblem("模拟学期超出了可规划范围。");

        var invalidCourse = selections.FirstOrDefault(x =>
            !context.CourseById.ContainsKey(x.CourseId));
        if (invalidCourse is not null)
            return ValidationProblem("模拟计划包含不属于当前培养方案的课程。");
        var alreadyHandled = selections.FirstOrDefault(x =>
            context.PassedCourseIds.Contains(x.CourseId) ||
            context.InProgressCourseIds.Contains(x.CourseId));
        if (alreadyHandled is not null)
            return ValidationProblem("已完成或当前在读课程无需重复安排。");

        var plannedSemesters = selections.ToDictionary(
            x => x.CourseId,
            x => x.Semester);
        var projectedCourseIds = context.PassedCourseIds
            .Concat(context.InProgressCourseIds)
            .Concat(plannedSemesters.Keys)
            .ToHashSet();
        var completion = CurriculumCompletionRules.Evaluate(
            context.Plan.Modules,
            projectedCourseIds);
        var addedCredits = context.Courses
            .Where(x =>
                !context.PassedCourseIds.Contains(x.CourseId) &&
                (context.InProgressCourseIds.Contains(x.CourseId) ||
                 plannedSemesters.ContainsKey(x.CourseId)))
            .Sum(x => x.Credits);
        var projectedEarnedCredits = context.EarnedCredits + addedCredits;
        var creditGap = Math.Max(
            context.Plan.TotalCredits - projectedEarnedCredits,
            0);
        var prerequisiteIssues = AcademicPlanningRules.FindPrerequisiteIssues(
            context.CourseSnapshots,
            context.PassedCourseIds,
            context.InProgressCourseIds,
            plannedSemesters);
        var conflicts = prerequisiteIssues.Select(issue =>
        {
            var course = context.CourseById[issue.CourseId];
            var prerequisite = context.AllCourseLabels.GetValueOrDefault(
                issue.PrerequisiteCourseId,
                new CourseLabel(
                    issue.PrerequisiteCourseId,
                    "未知课程",
                    "未找到的先修课程"));
            return new
            {
                Type = "Prerequisite",
                issue.CourseId,
                course.CourseName,
                PrerequisiteCourseId = prerequisite.CourseId,
                PrerequisiteCourseName = prerequisite.CourseName,
                issue.PlannedSemester,
                issue.PrerequisitePlannedSemester,
                Message = issue.PrerequisitePlannedSemester.HasValue
                    ? $"《{prerequisite.CourseName}》必须安排在《{course.CourseName}》之前。"
                    : $"《{course.CourseName}》的先修课程《{prerequisite.CourseName}》尚未完成或安排。"
            };
        }).ToList();
        var workloadWarnings = request.Terms
            .Select(term => new
            {
                term.Semester,
                Credits = term.CourseIds.Sum(courseId =>
                    context.CourseById.GetValueOrDefault(courseId)?.Credits ?? 0)
            })
            .Where(x =>
                x.Credits > AcademicPlanningRules.HeavySemesterCreditLimit)
            .Select(x => new
            {
                Type = "Workload",
                x.Semester,
                x.Credits,
                Message =
                    $"第 {x.Semester} 学期安排了 {x.Credits:0.#} 学分，超过建议上限 {AcademicPlanningRules.HeavySemesterCreditLimit:0.#} 学分。"
            })
            .ToList();
        var timingWarnings = selections
            .Select(item => new
            {
                item.Semester,
                Course = context.CourseById[item.CourseId]
            })
            .Where(x => x.Semester < x.Course.RecommendedSemester)
            .Select(x => new
            {
                Type = "EarlyCourse",
                x.Course.CourseId,
                x.Course.CourseName,
                x.Semester,
                x.Course.RecommendedSemester,
                Message =
                    $"《{x.Course.CourseName}》早于培养方案建议学期修读，请确认课程开设条件。"
            })
            .ToList();

        var unresolvedFailedCourseCount = context.FailedCourseIds.Count(
            courseId => !projectedCourseIds.Contains(courseId));
        var conclusion = GraduationAuditRules.Evaluate(
            true,
            context.Student.Status,
            context.Plan.TotalCredits,
            projectedEarnedCredits,
            completion.RequirementCount,
            completion.PassedRequirementCount,
            unresolvedFailedCourseCount);
        var latestPlannedSemester = plannedSemesters.Count == 0
            ? context.NextSemester - 1
            : plannedSemesters.Values.Max();
        var estimatedSemester =
            AcademicPlanningRules.EstimateCompletionSemester(
                context.NextSemester,
                latestPlannedSemester,
                creditGap,
                completion.RequirementCount -
                completion.PassedRequirementCount);
        var remainingRequiredSemesterFloor = context.Courses
            .Where(course =>
                course.Type == CurriculumCourseType.Required &&
                !projectedCourseIds.Contains(course.CourseId))
            .Select(course => course.RecommendedSemester)
            .DefaultIfEmpty(estimatedSemester)
            .Max();
        estimatedSemester = Math.Max(
            estimatedSemester,
            remainingRequiredSemesterFloor);
        var planCompletedCredits = context.Courses
            .Where(x => projectedCourseIds.Contains(x.CourseId))
            .Sum(x => x.Credits);

        return Ok(new
        {
            Projected = new
            {
                EarnedCredits = projectedEarnedCredits,
                PlanCompletedCredits = planCompletedCredits,
                CreditGap = creditGap,
                CompletionRate = context.Plan.TotalCredits <= 0
                    ? 0
                    : Math.Min(100, Math.Round(
                        planCompletedCredits / context.Plan.TotalCredits * 100,
                        1)),
                completion.RequirementCount,
                completion.PassedRequirementCount,
                completion.MissingRequirements,
                UnresolvedFailedCourseCount = unresolvedFailedCourseCount,
                GraduationConclusion = conclusion,
                EstimatedGraduationSemester = estimatedSemester,
                EstimatedGraduationTerm = FormatSemester(
                    context.Student.EnrollmentYear,
                    estimatedSemester),
                IsBeyondStandard =
                    estimatedSemester >
                    context.Plan.Major!.SchoolingYears * 2
            },
            Conflicts = conflicts,
            Warnings = workloadWarnings.Cast<object>()
                .Concat(timingWarnings)
                .ToList(),
            TermSummaries = request.Terms
                .OrderBy(x => x.Semester)
                .Select(term => new
                {
                    term.Semester,
                    Label = FormatSemester(
                        context.Student.EnrollmentYear,
                        term.Semester),
                    CourseCount = term.CourseIds.Count,
                    Credits = term.CourseIds.Sum(courseId =>
                        context.CourseById.GetValueOrDefault(courseId)?.Credits ??
                        0)
                })
        });
    }

    private async Task<LoadResult> LoadAsync(CancellationToken cancellationToken)
    {
        var userId = currentUserDataScope.Current.UserId;
        var student = await db.Students.AsNoTracking()
            .Include(x => x.AdministrativeClass)
            .ThenInclude(x => x!.Major)
            .ThenInclude(x => x!.College)
            .FirstOrDefaultAsync(x => x.UserId == userId, cancellationToken);
        if (student is null)
            return new LoadResult(
                null,
                ConflictProblem("当前账号尚未关联学生档案。"));

        var plan = await db.CurriculumPlans.AsNoTracking()
            .AsSplitQuery()
            .Include(x => x.Major)
            .Include(x => x.Modules)
            .ThenInclude(x => x.Courses)
            .ThenInclude(x => x.Course)
            .ThenInclude(x => x!.Prerequisites)
            .ThenInclude(x => x.PrerequisiteCourse)
            .Where(x =>
                x.MajorId == student.AdministrativeClass!.MajorId &&
                x.EffectiveGrade == student.EnrollmentYear &&
                x.Status == CurriculumPlanStatus.Published)
            .OrderByDescending(x => x.PublishedAt)
            .FirstOrDefaultAsync(cancellationToken);
        if (plan is null)
            return new LoadResult(
                null,
                ConflictProblem(
                    $"{student.EnrollmentYear} 级{student.AdministrativeClass!.Major!.Name}尚未发布培养方案。"));

        var courses = plan.Modules
            .SelectMany(module => module.Courses.Select(item =>
                new PlanningCourse(
                    item.CourseId,
                    item.Course!.Code,
                    item.Course.Name,
                    item.Course.Credits,
                    item.RecommendedSemester,
                    item.Type,
                    module.Code,
                    module.Name,
                    item.Course.Prerequisites
                        .Select(prerequisite => new CourseLabel(
                            prerequisite.PrerequisiteCourseId,
                            prerequisite.PrerequisiteCourse!.Code,
                            prerequisite.PrerequisiteCourse.Name))
                        .OrderBy(x => x.CourseCode)
                        .ToList())))
            .GroupBy(x => x.CourseId)
            .Select(group => group.First())
            .ToList();
        var planCourseIds = courses.Select(x => x.CourseId).ToArray();

        var gradeAttempts = await db.GradeRecords.AsNoTracking()
            .Where(x =>
                x.StudentId == student.Id &&
                x.GradeSheet!.Status == GradeSheetStatus.Published)
            .Select(x => new GradeAttempt(
                x.GradeSheet!.TeachingTask!.CourseId,
                x.GradeSheet.TeachingTask.Course!.Credits,
                x.TotalScore,
                x.ExamStatus))
            .ToListAsync(cancellationToken);
        var passedCourseIds = gradeAttempts
            .Where(x => StudentCourseProgressRules.IsPassed(
                new StudentCourseAttemptSnapshot(x.TotalScore, x.ExamStatus)))
            .Select(x => x.CourseId)
            .Distinct()
            .ToHashSet();
        var failedCourseIds = gradeAttempts
            .GroupBy(x => x.CourseId)
            .Where(group => !group.Any(x =>
                StudentCourseProgressRules.IsPassed(
                    new StudentCourseAttemptSnapshot(
                        x.TotalScore,
                        x.ExamStatus))))
            .Select(group => group.Key)
            .ToHashSet();
        var earnedCredits = gradeAttempts
            .Where(x => passedCourseIds.Contains(x.CourseId))
            .GroupBy(x => x.CourseId)
            .Sum(group => group.Max(x => x.Credits));

        var inProgressCourseIds = await db.TeachingTasks.AsNoTracking()
            .Where(task =>
                task.Status == TeachingTaskStatus.Published &&
                task.AcademicTerm!.IsCurrent &&
                (task.Classes.Any(item =>
                     item.AdministrativeClassId ==
                     student.AdministrativeClassId) ||
                 db.CourseEnrollments.Any(enrollment =>
                     enrollment.StudentId == student.Id &&
                     enrollment.Status == CourseEnrollmentStatus.Enrolled &&
                     enrollment.CourseSelectionOffering!.TeachingTaskId ==
                     task.Id)))
            .Select(x => x.CourseId)
            .Distinct()
            .ToListAsync(cancellationToken);
        var inProgressSet = inProgressCourseIds
            .Where(planCourseIds.Contains)
            .Where(courseId => !passedCourseIds.Contains(courseId))
            .ToHashSet();

        var attemptsByCourse = gradeAttempts
            .Where(x => planCourseIds.Contains(x.CourseId))
            .GroupBy(x => x.CourseId)
            .ToDictionary(x => x.Key, x => x.ToList());
        var statuses = courses.ToDictionary(
            x => x.CourseId,
            x => StudentCourseProgressRules.Evaluate(
                attemptsByCourse.GetValueOrDefault(x.CourseId, [])
                    .Select(attempt => new StudentCourseAttemptSnapshot(
                        attempt.TotalScore,
                        attempt.ExamStatus)),
                inProgressSet.Contains(x.CourseId)).Status);
        var currentTerm = await db.AcademicTerms.AsNoTracking()
            .Where(x => x.IsCurrent)
            .OrderByDescending(x => x.StartDate)
            .FirstOrDefaultAsync(cancellationToken);
        var currentSemester = CalculateCurrentSemester(
            student.EnrollmentYear,
            currentTerm);
        var snapshots = courses.Select(x =>
            new AcademicPlanningCourseSnapshot(
                x.CourseId,
                x.CourseName,
                x.Credits,
                x.RecommendedSemester,
                x.Type,
                x.Prerequisites.Select(p => p.CourseId).ToArray()))
            .ToList();
        var allLabels = courses
            .Select(x => new CourseLabel(
                x.CourseId,
                x.CourseCode,
                x.CourseName))
            .Concat(courses.SelectMany(x => x.Prerequisites))
            .GroupBy(x => x.CourseId)
            .ToDictionary(x => x.Key, x => x.First());

        return new LoadResult(
            new PlanningContext(
                student,
                plan,
                courses,
                courses.ToDictionary(x => x.CourseId),
                snapshots,
                allLabels,
                passedCourseIds,
                inProgressSet,
                failedCourseIds,
                statuses,
                earnedCredits,
                currentSemester,
                currentSemester + 1),
            null);
    }

    private static int CalculateCurrentSemester(
        int enrollmentYear,
        AcademicTerm? currentTerm)
    {
        if (currentTerm is not null)
        {
            return Math.Max(
                1,
                currentTerm.Season == TermSeason.Autumn
                    ? (currentTerm.StartDate.Year - enrollmentYear) * 2 + 1
                    : (currentTerm.StartDate.Year - enrollmentYear - 1) * 2 + 2);
        }

        var today = DateOnly.FromDateTime(DateTime.Today);
        return Math.Max(
            1,
            today.Month >= 8
                ? (today.Year - enrollmentYear) * 2 + 1
                : (today.Year - enrollmentYear - 1) * 2 + 2);
    }

    private static string FormatSemester(int enrollmentYear, int semester)
    {
        var startYear = enrollmentYear + (semester - 1) / 2;
        var season = semester % 2 == 1 ? "秋季学期" : "春季学期";
        return $"{startYear}—{startYear + 1} 学年{season}";
    }

    private ActionResult ConflictProblem(string detail) =>
        Conflict(new ProblemDetails
        {
            Title = "无法进行学业规划",
            Detail = detail,
            Status = StatusCodes.Status409Conflict
        });

    private sealed record GradeAttempt(
        Guid CourseId,
        decimal Credits,
        decimal? TotalScore,
        GradeExamStatus ExamStatus);

    private sealed record CourseLabel(
        Guid CourseId,
        string CourseCode,
        string CourseName);

    private sealed record PlanningCourse(
        Guid CourseId,
        string CourseCode,
        string CourseName,
        decimal Credits,
        int RecommendedSemester,
        CurriculumCourseType Type,
        string ModuleCode,
        string ModuleName,
        IReadOnlyList<CourseLabel> Prerequisites);

    private sealed record PlanningContext(
        Student Student,
        CurriculumPlan Plan,
        IReadOnlyList<PlanningCourse> Courses,
        IReadOnlyDictionary<Guid, PlanningCourse> CourseById,
        IReadOnlyList<AcademicPlanningCourseSnapshot> CourseSnapshots,
        IReadOnlyDictionary<Guid, CourseLabel> AllCourseLabels,
        IReadOnlySet<Guid> PassedCourseIds,
        IReadOnlySet<Guid> InProgressCourseIds,
        IReadOnlySet<Guid> FailedCourseIds,
        IReadOnlyDictionary<Guid, StudentCourseProgressStatus> StatusByCourse,
        decimal EarnedCredits,
        int CurrentSemester,
        int NextSemester);

    private sealed record LoadResult(
        PlanningContext? Context,
        ActionResult? Error);
}

public sealed record AcademicPlanningRequest(
    IReadOnlyCollection<AcademicPlanningTermRequest> Terms);

public sealed record AcademicPlanningTermRequest(
    [Range(1, 30)] int Semester,
    IReadOnlyCollection<Guid> CourseIds);
