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

namespace Jiaowu.Api.Infrastructure.Scheduling;

public sealed class AutomaticScheduleGenerator(AppDbContext db)
{
    public Task<AutomaticScheduleResult> GenerateAsync(
        SchedulePlan plan,
        CancellationToken cancellationToken) =>
        GenerateAsync(plan, null, true, cancellationToken);

    public async Task<AutomaticScheduleResult> GenerateAsync(
        SchedulePlan plan,
        Func<AutomaticScheduleProgress, CancellationToken, Task>? reportProgress,
        bool saveChanges,
        CancellationToken cancellationToken)
    {
        var timeSlots = await db.ScheduleTimeSlots.AsNoTracking()
            .Where(x => x.AcademicTermId == plan.AcademicTermId && x.IsEnabled)
            .OrderBy(x => x.PeriodNumber)
            .ToListAsync(cancellationToken);
        if (timeSlots.Count == 0)
            return new(0, 0, ["请先维护该学期的上课时间表。"]);

        var activePeriods = timeSlots.Select(x => x.PeriodNumber).ToHashSet();
        var tasks = await db.TeachingTasks
             .Where(x =>
                 x.AcademicTermId == plan.AcademicTermId &&
                 x.Status == TeachingTaskStatus.Published &&
                 x.SchedulingMode == TeachingTaskSchedulingMode.Standard)
            .Include(x => x.Teachers)
            .Include(x => x.Classes)
            .ThenInclude(x => x.AdministrativeClass)
            .ThenInclude(x => x!.Students)
            .Include(x => x.Course)
            .OrderByDescending(x => x.Classes.Count + x.Teachers.Count)
            .ThenByDescending(x => x.Capacity)
            .ThenBy(x => x.TaskNumber)
            .ToListAsync(cancellationToken);
        var taskIds = tasks.Select(task => task.Id).ToArray();
        var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
            .WhereIn(taskIds, x => x.TeachingTaskId)
            .Include(x => x.AllowedClassrooms)
            .Include(x => x.AllowedExperimentClassrooms)
            .ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
        var classrooms = await db.Classrooms.AsNoTracking()
            .Where(x => x.IsEnabled)
            .Include(x => x.Building)
            .OrderBy(x => x.Capacity)
            .ThenBy(x => x.Name)
            .ToListAsync(cancellationToken);
        var entries = await db.ScheduleEntries
            .Where(x => x.SchedulePlanId == plan.Id)
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.Teachers)
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.Classes)
            .ToListAsync(cancellationToken);

        var created = 0;
        var completedTasks = 0;
        var processedTasks = 0;
        var messages = new List<string>();
        if (reportProgress is not null)
        {
            await reportProgress(
                new(tasks.Count, 0, 0, 0),
                cancellationToken);
        }

        foreach (var task in tasks)
        {
            cancellationToken.ThrowIfCancellationRequested();
            constraints.TryGetValue(task.Id, out var constraint);
            var taskCompleted = true;
            foreach (var kind in new[]
                     {
                         ScheduleEntryKind.Lecture,
                         ScheduleEntryKind.Experiment
                     })
            {
                var targetHours = TeachingTaskHours.TargetHours(task.Course!, kind);
                var scheduledHours = entries
                    .Where(x =>
                        x.TeachingTaskId == task.Id &&
                        x.Kind == kind)
                    .Sum(TeachingTaskHours.ScheduledHours);
                var label = kind == ScheduleEntryKind.Experiment ? "实验课" : "理论课";
                if (scheduledHours > targetHours)
                {
                    messages.Add(
                        $"{task.TaskNumber} · {task.Name} 的{label}已安排 {scheduledHours} 学时，" +
                        $"超过课程规定的 {targetHours} 学时，请先删除多余课次。");
                    taskCompleted = false;
                    continue;
                }

                var remainingHours = targetHours - scheduledHours;
                while (remainingHours > 0)
                {
                    var candidate = FindBestCandidateForHours(
                        plan.Id,
                        task,
                        constraint,
                        kind,
                        remainingHours,
                        activePeriods,
                        classrooms,
                        entries,
                        cancellationToken);
                    if (candidate is null) break;

                    db.ScheduleEntries.Add(candidate);
                    entries.Add(candidate);
                    created++;
                    remainingHours -= TeachingTaskHours.ScheduledHours(candidate);
                }

                if (remainingHours > 0)
                {
                    messages.Add(
                        $"{task.TaskNumber} · {task.Name} 仍有 {remainingHours} 个{label}学时无法安排，" +
                        (kind == ScheduleEntryKind.Experiment
                            ? "请检查实验室/机房容量、教师班级冲突或时间约束。"
                            : "请检查教师/班级冲突或场地与时间约束。"));
                    taskCompleted = false;
                }
            }

            if (taskCompleted) completedTasks++;

            processedTasks++;
            if (reportProgress is not null)
            {
                await reportProgress(
                    new(tasks.Count, processedTasks, created, completedTasks),
                    cancellationToken);
            }
        }

        if (created > 0 && saveChanges)
            await db.SaveChangesAsync(cancellationToken);
        return new(
            created,
            completedTasks,
            messages,
            tasks.Count,
            processedTasks);
    }

    private static ScheduleEntry? FindBestCandidateForHours(
        Guid planId,
        TeachingTask task,
        TeachingTaskScheduleConstraint? constraint,
        ScheduleEntryKind kind,
        int remainingHours,
        HashSet<int> activePeriods,
        IReadOnlyList<Classroom> classrooms,
        IReadOnlyList<ScheduleEntry> entries,
        CancellationToken cancellationToken)
    {
        var weekCount = task.EndWeek - task.StartWeek + 1;
        foreach (var periodCount in remainingHours >= 2
                     ? new[] { 2, 1 }
                     : new[] { 1 })
        {
            var maxOccurrences = Math.Min(
                weekCount,
                remainingHours / periodCount);
            for (var occurrences = maxOccurrences; occurrences >= 1; occurrences--)
            {
                var candidate = FindBestCandidate(
                    planId,
                    task,
                    constraint,
                    kind,
                    periodCount,
                    occurrences,
                    activePeriods,
                    classrooms,
                    entries,
                    cancellationToken);
                if (candidate is not null) return candidate;
            }
        }
        return null;
    }

    private static ScheduleEntry? FindBestCandidate(
        Guid planId,
        TeachingTask task,
        TeachingTaskScheduleConstraint? constraint,
        ScheduleEntryKind kind,
        int periodCount,
        int occurrenceCount,
        HashSet<int> activePeriods,
        IReadOnlyList<Classroom> classrooms,
        IReadOnlyList<ScheduleEntry> entries,
        CancellationToken cancellationToken)
    {
        var allowedDays = ParseAllowedDays(constraint?.AllowedDayOfWeeks);
        var firstPeriod = constraint?.EarliestPeriod ?? activePeriods.Min();
        var lastPeriod = constraint?.LatestPeriod ?? activePeriods.Max();
        var rooms = EligibleRooms(task, constraint, kind, classrooms);
        if ((constraint?.RequiresClassroom ?? true) && rooms.Count == 0)
            return null;

        var candidates = new List<(ScheduleEntry Entry, int Score)>();
        for (var startWeek = task.StartWeek;
             startWeek + occurrenceCount - 1 <= task.EndWeek;
             startWeek++)
        {
            foreach (var day in allowedDays)
            {
                cancellationToken.ThrowIfCancellationRequested();
                for (var start = firstPeriod; start + periodCount - 1 <= lastPeriod; start++)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    if (Enumerable.Range(start, periodCount).Any(period => !activePeriods.Contains(period)))
                        continue;

                    var roomOptions = kind != ScheduleEntryKind.Experiment &&
                                      constraint?.RequiresClassroom == false
                        ? new Classroom?[] { null }
                        : rooms.Cast<Classroom?>().ToArray();
                    foreach (var room in roomOptions)
                    {
                        var proposed = new ScheduleEntry
                        {
                            SchedulePlanId = planId,
                            TeachingTaskId = task.Id,
                            TeachingTask = task,
                            Kind = kind,
                            ClassroomId = room?.Id,
                            DayOfWeek = day,
                            StartPeriod = start,
                            PeriodCount = periodCount,
                            StartWeek = startWeek,
                            EndWeek = startWeek + occurrenceCount - 1,
                            WeekPattern = WeekPattern.All,
                            Notes = kind == ScheduleEntryKind.Experiment
                                ? "自动排课 · 实验课"
                                : "自动排课 · 理论课"
                        };
                        if (entries.Any(existing =>
                                ScheduleConflictDetector.TimeOverlaps(existing, proposed) &&
                                ScheduleConflictDetector.ConflictReason(existing, proposed) is not null))
                            continue;

                        var sameTaskDay = entries.Count(x =>
                            x.TeachingTaskId == task.Id && x.DayOfWeek == day);
                        var dayLoad = entries.Count(x => x.DayOfWeek == day);
                        var roomWaste = room is null ? 0 : Math.Max(0, room.Capacity - task.Capacity);
                        var experimentGeneralClassroomPenalty =
                            kind == ScheduleEntryKind.Experiment &&
                            constraint?.AllowedExperimentVenueNatures == 0 &&
                            room?.TeachingVenueNature == TeachingVenueNature.GeneralClassroom
                                ? 100_000
                                : 0;
                        var score = sameTaskDay * 1000 + dayLoad * 10 + start +
                                    roomWaste / 10 + startWeek +
                                    experimentGeneralClassroomPenalty;
                        candidates.Add((proposed, score));
                    }
                }
            }
        }
        return candidates
            .OrderBy(x => x.Score)
            .ThenBy(x => x.Entry.DayOfWeek)
            .ThenBy(x => x.Entry.StartPeriod)
            .Select(x => x.Entry)
            .FirstOrDefault();
    }

    private static IReadOnlyList<Classroom> EligibleRooms(
        TeachingTask task,
        TeachingTaskScheduleConstraint? constraint,
        ScheduleEntryKind kind,
        IReadOnlyList<Classroom> classrooms)
    {
        if (kind != ScheduleEntryKind.Experiment &&
            constraint?.RequiresClassroom == false) return [];
        var allowedRoomIds = constraint?.AllowedClassrooms
            .Select(x => x.ClassroomId)
            .ToHashSet() ?? [];
        var allowedExperimentRoomIds = constraint?.AllowedExperimentClassrooms
            .Select(x => x.ClassroomId)
            .ToHashSet() ?? [];
        var minimumCapacity = Math.Max(
            task.Capacity,
            task.Classes.Sum(x =>
                x.AdministrativeClass?.Students.Count(student =>
                    student.Status == StudentStatus.Active) ?? 0));
        return classrooms.Where(room =>
                room.Capacity >= minimumCapacity &&
                (kind == ScheduleEntryKind.Experiment ||
                 constraint?.RequiredCampusId is not Guid requiredCampusId ||
                    room.Building!.CampusId == requiredCampusId) &&
                (kind == ScheduleEntryKind.Experiment ||
                 constraint?.RequiredBuildingId is not Guid requiredBuildingId ||
                    room.BuildingId == requiredBuildingId) &&
                (kind != ScheduleEntryKind.Experiment ||
                 constraint?.ExperimentRequiredCampusId is not Guid experimentCampusId ||
                 room.Building!.CampusId == experimentCampusId) &&
                (kind != ScheduleEntryKind.Experiment ||
                 constraint?.ExperimentRequiredBuildingId is not Guid experimentBuildingId ||
                 room.BuildingId == experimentBuildingId) &&
                (kind == ScheduleEntryKind.Experiment ||
                 allowedRoomIds.Count == 0 || allowedRoomIds.Contains(room.Id)) &&
                (kind != ScheduleEntryKind.Experiment || constraint is null ||
                  constraint.AllowedExperimentVenueNatures == 0 ||
                  (room.TeachingVenueNature & constraint.AllowedExperimentVenueNatures) != 0) &&
                 (kind != ScheduleEntryKind.Experiment || allowedExperimentRoomIds.Count == 0 ||
                  allowedExperimentRoomIds.Contains(room.Id)))
            .ToList();
    }


    private static int[] ParseAllowedDays(string? value)
    {
        if (string.IsNullOrWhiteSpace(value)) return [1, 2, 3, 4, 5];
        var days = value.Split(',', StringSplitOptions.RemoveEmptyEntries)
            .Select(item => int.TryParse(item, out var day) ? day : 0)
            .Where(day => day is >= 1 and <= 7)
            .Distinct()
            .Order()
            .ToArray();
        return days.Length == 0 ? [1, 2, 3, 4, 5] : days;
    }
}

public sealed record AutomaticScheduleResult(
    int CreatedEntries,
    int CompletedTasks,
    IReadOnlyList<string> Messages,
    int TotalTasks = 0,
    int ProcessedTasks = 0);

public sealed record AutomaticScheduleProgress(
    int TotalTasks,
    int ProcessedTasks,
    int CreatedEntries,
    int CompletedTasks);
