using System.Diagnostics.CodeAnalysis;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Timetables;
using Jiaowu.Api.Infrastructure.Teaching;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Infrastructure.Scheduling;

public sealed class SchedulePublishJobProcessor(
    AppDbContext db,
    SchedulePlanPublisher publisher,
    IAppCache cache,
    ILogger<SchedulePublishJobProcessor> logger)
{
    public async Task ProcessAsync(Guid jobId, CancellationToken stoppingToken)
    {
        try
        {
            var job = await db.SchedulePublishJobs
                .FirstOrDefaultAsync(x => x.Id == jobId, stoppingToken);
            if (job is null ||
                job.Status is SchedulePublishJobStatus.Succeeded
                    or SchedulePublishJobStatus.Failed)
            {
                return;
            }

            job.Status = SchedulePublishJobStatus.Running;
            job.StartedAt = DateTime.UtcNow;
            job.CompletedAt = null;
            job.CompletedSteps = 0;
            job.CurrentStep = "读取排课版本";
            job.ErrorMessage = null;
            await db.SaveChangesAsync(stoppingToken);

            async Task ReportProgress(
                int completedSteps,
                string currentStep,
                CancellationToken cancellationToken)
            {
                job.CompletedSteps = completedSteps;
                job.CurrentStep = currentStep;
                await db.SaveChangesAsync(cancellationToken);
            }

            var plan = await publisher.ValidateAsync(
                job.SchedulePlanId,
                ReportProgress,
                stoppingToken);

            var publishedPlanId = plan.Id;
            await db.ExecuteInRetriableTransactionAsync(
                async transaction =>
                {
                    db.ChangeTracker.Clear();
                    var publishJob = await db.SchedulePublishJobs
                        .FirstAsync(x => x.Id == jobId, stoppingToken);
                    var publishPlan = await db.SchedulePlans
                        .FirstAsync(x => x.Id == publishedPlanId, stoppingToken);
                    if (publishPlan.Status != SchedulePlanStatus.Draft)
                    {
                        throw new SchedulePublishValidationException(
                            "排课草稿状态已发生变化，请刷新后重试。");
                    }

                    var previous = await db.SchedulePlans
                        .Where(x =>
                            x.Id != publishPlan.Id &&
                            x.AcademicTermId == publishPlan.AcademicTermId &&
                            x.Status == SchedulePlanStatus.Published)
                        .ToListAsync(stoppingToken);
                    foreach (var oldPlan in previous)
                        oldPlan.Status = SchedulePlanStatus.Archived;

                    await new PublishedTimetableProjectionService(db)
                        .RebuildAsync(publishPlan, stoppingToken);
                    publishPlan.Status = SchedulePlanStatus.Published;
                    publishPlan.PublishedAt = DateTime.UtcNow;
                    publishJob.Status = SchedulePublishJobStatus.Succeeded;
                    publishJob.ActiveAcademicTermId = null;
                    publishJob.CompletedSteps = publishJob.TotalSteps;
                    publishJob.CurrentStep = "课表已发布";
                    publishJob.CompletedAt = DateTime.UtcNow;
                    await db.SaveChangesAsync(stoppingToken);
                    await transaction.CommitAsync(stoppingToken);
                },
                stoppingToken);

            await cache.RemoveByTagAsync(AppCacheTags.Timetables, stoppingToken);
            logger.LogInformation(
                "Schedule publish job {JobId} published plan {SchedulePlanId}.",
                jobId,
                publishedPlanId);
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            logger.LogInformation(
                "Schedule publish job {JobId} was interrupted by application shutdown.",
                jobId);
            throw;
        }
        catch (Exception exception)
        {
            logger.LogError(exception, "Schedule publish job {JobId} failed.", jobId);
            await MarkFailedAsync(jobId, exception);
        }
    }

    private async Task MarkFailedAsync(Guid jobId, Exception exception)
    {
        db.ChangeTracker.Clear();
        var job = await db.SchedulePublishJobs.FirstOrDefaultAsync(
            x => x.Id == jobId,
            CancellationToken.None);
        if (job is null)
            return;

        var message = exception.GetBaseException().Message;
        job.Status = SchedulePublishJobStatus.Failed;
        job.ActiveAcademicTermId = null;
        job.CurrentStep = "检查未通过";
        job.ErrorMessage = message.Length <= 2000 ? message : message[..2000];
        job.CompletedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(CancellationToken.None);
    }
}

public sealed class SchedulePlanPublisher(AppDbContext db)
{
    public async Task<SchedulePlan> ValidateAsync(
        Guid planId,
        Func<int, string, CancellationToken, Task> reportProgress,
        CancellationToken cancellationToken)
    {
        var plan = await db.SchedulePlans
            .AsSplitQuery()
            .Include(x => x.Entries)
            .ThenInclude(x => x.Classroom)
            .ThenInclude(x => x!.Building)
            .Include(x => x.Entries)
            .ThenInclude(x => x.TeachingTask)
            .ThenInclude(x => x!.Teachers)
            .Include(x => x.Entries)
            .ThenInclude(x => x.TeachingTask)
            .ThenInclude(x => x!.Classes)
            .ThenInclude(x => x.AdministrativeClass)
            .ThenInclude(x => x!.Students)
            .FirstOrDefaultAsync(x => x.Id == planId, cancellationToken)
            ?? throw new SchedulePublishValidationException("排课草稿不存在。");

        if (plan.Status != SchedulePlanStatus.Draft)
            throw new SchedulePublishValidationException("只有草稿排课版本可以发布。");
        if (plan.Entries.Count == 0)
            throw new SchedulePublishValidationException(
                "排课版本中至少需要一条课表安排。");

        await reportProgress(1, "校验课程、节次与教室", cancellationToken);
        var activePeriods = (await db.ScheduleTimeSlots.AsNoTracking()
                .Where(x => x.AcademicTermId == plan.AcademicTermId && x.IsEnabled)
                .Select(x => x.PeriodNumber)
                .ToListAsync(cancellationToken))
            .ToHashSet();
        if (activePeriods.Count == 0)
            throw new SchedulePublishValidationException(
                "请先维护该学期的上课时间表。");

        var taskIds = plan.Entries.Select(x => x.TeachingTaskId).Distinct().ToList();
        var constraints = await db.TeachingTaskScheduleConstraints.AsNoTracking()
            .WhereIn(taskIds, x => x.TeachingTaskId)
            .Include(x => x.AllowedClassrooms)
            .Include(x => x.AllowedExperimentClassrooms)
            .ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);

        foreach (var entry in plan.Entries)
        {
            ValidateEntry(plan, entry, activePeriods, constraints);
        }

        await reportProgress(2, "校验教学任务完整性", cancellationToken);
        var requiredTasks = await db.TeachingTasks.AsNoTracking()
            .Where(x =>
                x.AcademicTermId == plan.AcademicTermId &&
                x.Status == TeachingTaskStatus.Published &&
                x.SchedulingMode == TeachingTaskSchedulingMode.Standard)
            .Select(x => new
            {
                x.Id,
                x.TaskNumber,
                x.Name,
                x.StartWeek,
                x.EndWeek,
                CourseTotalHours = x.Course!.TotalHours,
                CoursePracticeHours = x.Course.PracticeHours
            })
            .ToListAsync(cancellationToken);
        var scheduledHours = plan.Entries
            .GroupBy(x => new { x.TeachingTaskId, x.Kind })
            .ToDictionary(
                group => (group.Key.TeachingTaskId, group.Key.Kind),
                group => group.Sum(TeachingTaskHours.ScheduledHours));
        foreach (var task in requiredTasks)
        {
            var targets = new[]
            {
                (Kind: ScheduleEntryKind.Lecture,
                    Hours: Math.Max(0, task.CourseTotalHours - task.CoursePracticeHours),
                    Label: "理论课"),
                (Kind: ScheduleEntryKind.Experiment,
                    Hours: Math.Max(0, task.CoursePracticeHours),
                    Label: "实验课")
            };
            foreach (var target in targets)
            {
                var actual = scheduledHours.GetValueOrDefault((task.Id, target.Kind));
                if (actual == target.Hours) continue;
                throw new SchedulePublishValidationException(
                    $"{task.TaskNumber} · {task.Name} 的{target.Label}应安排 " +
                    $"{target.Hours} 学时，当前已安排 {actual} 学时，不能发布。");
            }
        }

        await reportProgress(3, "检查教师、行政班和教室冲突", cancellationToken);
        var conflict = ScheduleConflictDetector.FindConflict(plan.Entries.ToList());
        if (conflict is not null)
            throw new SchedulePublishValidationException(conflict);

        await reportProgress(4, "写入正式课表", cancellationToken);
        return plan;
    }

    private static void ValidateEntry(
        SchedulePlan plan,
        ScheduleEntry entry,
        HashSet<int> activePeriods,
        IReadOnlyDictionary<Guid, TeachingTaskScheduleConstraint> constraints)
    {
        var task = entry.TeachingTask;
        if (entry.StartWeek > entry.EndWeek)
            Fail(entry, "开始周不能晚于结束周");
        if (Enumerable.Range(entry.StartPeriod, entry.PeriodCount)
            .Any(period => !activePeriods.Contains(period)))
            Fail(entry, "所选节次包含未启用或不存在的上课时间");
        if (task is null ||
            task.Status != TeachingTaskStatus.Published ||
            task.AcademicTermId != plan.AcademicTermId)
            Fail(entry, "只能安排同一学期内已发布的教学任务");
        if (task.SchedulingMode == TeachingTaskSchedulingMode.Flexible)
            Fail(entry, "非排时课程不应进入正常课表");
        if (entry.StartWeek < task.StartWeek || entry.EndWeek > task.EndWeek)
            Fail(entry, "排课周次不在教学任务的授课周次内");

        constraints.TryGetValue(entry.TeachingTaskId, out var constraint);
        var requiresClassroom = entry.Kind == ScheduleEntryKind.Experiment ||
                                constraint?.RequiresClassroom != false;
        if (requiresClassroom && entry.ClassroomId is null)
            Fail(entry, "该课程需要占用教室");
        if (!requiresClassroom && entry.ClassroomId is not null)
            Fail(entry, "该课程已设置为不占用教室");

        var allowedDays = ParseDays(constraint?.AllowedDayOfWeeks);
        if (allowedDays.Count > 0 && !allowedDays.Contains(entry.DayOfWeek))
            Fail(entry, "上课日不在教学任务允许范围内");
        if (constraint?.EarliestPeriod is int earliest &&
            entry.StartPeriod < earliest)
            Fail(entry, $"最早只能从第 {earliest} 节开始");
        if (constraint?.LatestPeriod is int latest &&
            entry.StartPeriod + entry.PeriodCount - 1 > latest)
            Fail(entry, $"最晚必须在第 {latest} 节结束");

        var classroom = entry.Classroom;
        if (entry.ClassroomId.HasValue)
        {
            if (classroom is null || !classroom.IsEnabled)
                Fail(entry, "所选教室不存在或已停用");
            if (entry.Kind != ScheduleEntryKind.Experiment &&
                constraint?.RequiredCampusId is Guid campusId &&
                classroom.Building!.CampusId != campusId)
                Fail(entry, "所选教室不在指定校区");
            if (entry.Kind != ScheduleEntryKind.Experiment &&
                constraint?.RequiredBuildingId is Guid buildingId &&
                classroom.BuildingId != buildingId)
                Fail(entry, "所选教室不在指定教学楼");
            if (entry.Kind == ScheduleEntryKind.Experiment &&
                constraint?.ExperimentRequiredCampusId is Guid experimentCampusId &&
                classroom.Building!.CampusId != experimentCampusId)
                Fail(entry, "所选场地不在实验课指定校区");
            if (entry.Kind == ScheduleEntryKind.Experiment &&
                constraint?.ExperimentRequiredBuildingId is Guid experimentBuildingId &&
                classroom.BuildingId != experimentBuildingId)
                Fail(entry, "所选场地不在实验课指定教学楼");
            var allowedClassroomIds = constraint?.AllowedClassrooms
                .Select(x => x.ClassroomId)
                .ToHashSet() ?? [];
            if (entry.Kind != ScheduleEntryKind.Experiment && allowedClassroomIds.Count > 0 &&
                !allowedClassroomIds.Contains(classroom.Id))
                Fail(entry, "所选教室不在指定教室范围内");
            var allowedExperimentClassroomIds = constraint?.AllowedExperimentClassrooms
                .Select(x => x.ClassroomId)
                .ToHashSet() ?? [];
            if (entry.Kind == ScheduleEntryKind.Experiment &&
                allowedExperimentClassroomIds.Count > 0 &&
                !allowedExperimentClassroomIds.Contains(classroom.Id))
                Fail(entry, "所选场地不在实验课指定场地范围内");
            if (entry.Kind == ScheduleEntryKind.Experiment &&
                constraint?.AllowedExperimentVenueNatures is { } allowedNatures &&
                allowedNatures != 0 &&
                (classroom.TeachingVenueNature & allowedNatures) == 0)
                Fail(entry, "所选场地不在实验课允许的场地性质范围内");
        }

        var studentCount = task.Classes.Sum(x =>
            x.AdministrativeClass!.Students.Count(student =>
                student.Status == StudentStatus.Active));
        var requiredCapacity = Math.Max(task.Capacity, studentCount);
        if (classroom is not null && requiredCapacity > classroom.Capacity)
        {
            Fail(entry,
                $"教室容量不足：需要 {requiredCapacity} 人，教室仅容纳 {classroom.Capacity} 人");
        }
    }

    private static HashSet<int> ParseDays(string? value) =>
        string.IsNullOrWhiteSpace(value)
            ? []
            : value.Split(',', StringSplitOptions.RemoveEmptyEntries)
                .Select(int.Parse)
                .ToHashSet();

    [DoesNotReturn]
    private static void Fail(ScheduleEntry entry, string message)
    {
        var name = entry.TeachingTask?.Name ?? entry.TeachingTaskId.ToString();
        throw new SchedulePublishValidationException($"“{name}”：{message}。");
    }
}

public sealed class SchedulePublishValidationException(string message)
    : InvalidOperationException(message);
