using Jiaowu.Api.Domain.Academic;

namespace Jiaowu.Api.Infrastructure.Graduation;

public sealed record AcademicPlanningCourseSnapshot(
    Guid CourseId,
    string CourseName,
    decimal Credits,
    int RecommendedSemester,
    CurriculumCourseType Type,
    IReadOnlyCollection<Guid> PrerequisiteCourseIds);

public sealed record AcademicPlanningPrerequisiteIssue(
    Guid CourseId,
    Guid PrerequisiteCourseId,
    int PlannedSemester,
    int? PrerequisitePlannedSemester);

public static class AcademicPlanningRules
{
    public const decimal RecommendedSemesterCreditLimit = 24;
    public const decimal HeavySemesterCreditLimit = 30;

    public static IReadOnlyList<AcademicPlanningPrerequisiteIssue>
        FindPrerequisiteIssues(
            IEnumerable<AcademicPlanningCourseSnapshot> courses,
            IReadOnlySet<Guid> completedCourseIds,
            IReadOnlySet<Guid> inProgressCourseIds,
            IReadOnlyDictionary<Guid, int> plannedSemesters)
    {
        var issues = new List<AcademicPlanningPrerequisiteIssue>();
        foreach (var course in courses.Where(x =>
                     plannedSemesters.ContainsKey(x.CourseId)))
        {
            var plannedSemester = plannedSemesters[course.CourseId];
            foreach (var prerequisiteId in course.PrerequisiteCourseIds)
            {
                if (completedCourseIds.Contains(prerequisiteId) ||
                    inProgressCourseIds.Contains(prerequisiteId))
                    continue;

                if (!plannedSemesters.TryGetValue(
                        prerequisiteId,
                        out var prerequisiteSemester) ||
                    prerequisiteSemester >= plannedSemester)
                {
                    issues.Add(new AcademicPlanningPrerequisiteIssue(
                        course.CourseId,
                        prerequisiteId,
                        plannedSemester,
                        plannedSemesters.GetValueOrDefault(prerequisiteId)));
                }
            }
        }
        return issues;
    }

    public static IReadOnlyList<Guid> SuggestNextSemester(
        IEnumerable<AcademicPlanningCourseSnapshot> courses,
        IReadOnlySet<Guid> completedCourseIds,
        IReadOnlySet<Guid> inProgressCourseIds,
        int nextSemester,
        decimal targetCredits = RecommendedSemesterCreditLimit)
    {
        var available = courses
            .Where(x =>
                !completedCourseIds.Contains(x.CourseId) &&
                !inProgressCourseIds.Contains(x.CourseId) &&
                x.RecommendedSemester <= nextSemester &&
                x.PrerequisiteCourseIds.All(prerequisiteId =>
                    completedCourseIds.Contains(prerequisiteId) ||
                    inProgressCourseIds.Contains(prerequisiteId)))
            .OrderBy(x => x.Type == CurriculumCourseType.Required ? 0 : 1)
            .ThenBy(x => x.RecommendedSemester > nextSemester ? 1 : 0)
            .ThenBy(x => x.RecommendedSemester)
            .ThenBy(x => x.CourseName)
            .ToList();

        var selected = new List<Guid>();
        decimal credits = 0;
        foreach (var course in available)
        {
            var isOverdueRequired =
                course.Type == CurriculumCourseType.Required &&
                course.RecommendedSemester <= nextSemester;
            if (!isOverdueRequired &&
                selected.Count > 0 &&
                credits + course.Credits > targetCredits)
                continue;

            selected.Add(course.CourseId);
            credits += course.Credits;
            if (credits >= targetCredits) break;
        }
        return selected;
    }

    public static int EstimateCompletionSemester(
        int nextSemester,
        int latestPlannedSemester,
        decimal remainingCredits,
        int remainingRequirementCount)
    {
        if (remainingCredits <= 0 && remainingRequirementCount <= 0)
            return Math.Max(nextSemester - 1, latestPlannedSemester);

        var byCredits = (int)Math.Ceiling(
            Math.Max(remainingCredits, 0) / RecommendedSemesterCreditLimit);
        var byRequirements = (int)Math.Ceiling(
            Math.Max(remainingRequirementCount, 0) / 6m);
        var additionalSemesters = Math.Max(1, Math.Max(byCredits, byRequirements));
        return Math.Max(nextSemester - 1, latestPlannedSemester) +
               additionalSemesters;
    }
}
