using System.ComponentModel.DataAnnotations;
using System.Security.Claims;
using System.Text.Json;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Domain.System;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Scheduling;
using Jiaowu.Api.Infrastructure.Teaching;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Controllers;

[ApiController]
[Authorize(Roles = ManagementRoles)]
[Route("api/schedules")]
public sealed class SchedulesController(
    AppDbContext db) : ControllerBase
{
    private const string ManagementRoles =
        SystemRoles.SuperAdmin + "," +
        SystemRoles.AcademicAdmin;

    [HttpGet("plans")]
    public async Task<ActionResult> GetPlans(
        Guid? academicTermId,
        CancellationToken cancellationToken)
    {
        var source = db.SchedulePlans.AsNoTracking().AsQueryable();
        if (academicTermId.HasValue)
            source = source.Where(x => x.AcademicTermId == academicTermId);
        return Ok(await source
            .OrderByDescending(x => x.AcademicTerm!.StartDate)
            .ThenByDescending(x => x.CreatedAt)
            .Select(x => new
            {
                x.Id,
                x.Name,
                x.Version,
                x.AcademicTermId,
                TermName = x.AcademicTerm!.Name,
                x.Status,
                x.PublishedAt,
                EntryCount = x.Entries.Count,
                x.UpdatedAt
            })
            .ToListAsync(cancellationToken));
    }

    [HttpGet("plans/{id:guid}")]
    public async Task<ActionResult> GetPlan(Guid id, CancellationToken cancellationToken)
    {
        var plan = await db.SchedulePlans.AsNoTracking()
            .Where(x => x.Id == id)
            .Select(x => new
            {
                x.Id,
                x.Name,
                x.Version,
                x.AcademicTermId,
                TermName = x.AcademicTerm!.Name,
                x.Status,
                x.Notes,
                x.PublishedAt,
                Entries = x.Entries
                    .OrderBy(entry => entry.DayOfWeek)
                    .ThenBy(entry => entry.StartPeriod)
                    .Select(entry => new
                    {
                        entry.Id,
                        entry.TeachingTaskId,
                        entry.Kind,
                        TaskNumber = entry.TeachingTask!.TaskNumber,
                        TaskName = entry.TeachingTask.Name,
                        CourseCode = entry.TeachingTask.Course!.Code,
                        CourseName = entry.TeachingTask.Course.Name,
                        TeacherNames = entry.TeachingTask.Teachers
                            .OrderByDescending(item => item.IsPrimary)
                            .Select(item => item.Teacher!.Name),
                        ClassNames = entry.TeachingTask.Classes
                            .Select(item => item.AdministrativeClass!.Name),
                        entry.ClassroomId,
                        ClassroomName = entry.Classroom == null
                            ? null
                            : entry.Classroom.Name,
                        BuildingName = entry.Classroom == null
                            ? null
                            : entry.Classroom.Building!.Name,
                        CampusName = entry.Classroom == null
                            ? null
                            : entry.Classroom.Building!.Campus!.Name,
                        entry.DayOfWeek,
                        entry.StartPeriod,
                        entry.PeriodCount,
                        entry.StartWeek,
                        entry.EndWeek,
                        entry.WeekPattern,
                        entry.Notes
                    })
            })
            .FirstOrDefaultAsync(cancellationToken);
        return plan is null ? NotFound() : Ok(plan);
    }

    [HttpPost("plans")]
    public async Task<ActionResult> CreatePlan(
        SchedulePlanRequest request,
        CancellationToken cancellationToken)
    {
        if (!await db.AcademicTerms.AnyAsync(
                x => x.Id == request.AcademicTermId && x.IsEnabled,
                cancellationToken))
            return ValidationProblem("所选学期不存在或已停用。");
        var plan = new SchedulePlan
        {
            AcademicTermId = request.AcademicTermId,
            Name = request.Name.Trim(),
            Version = request.Version.Trim(),
            Notes = Normalize(request.Notes)
        };
        db.SchedulePlans.Add(plan);
        return await SaveAsync(plan.Id, true, cancellationToken);
    }

    [HttpPut("plans/{id:guid}")]
    public async Task<ActionResult> UpdatePlan(
        Guid id,
        SchedulePlanRequest request,
        CancellationToken cancellationToken)
    {
        var plan = await db.SchedulePlans.FindAsync([id], cancellationToken);
        if (plan is null) return NotFound();
        if (await HasActiveScheduleJobAsync(id, cancellationToken))
            return ScheduleJobRunningProblem();
        if (plan.Status != SchedulePlanStatus.Draft)
            return ConflictProblem("已发布或已归档的排课版本不可直接修改。");
        if (!await db.AcademicTerms.AnyAsync(
                x => x.Id == request.AcademicTermId && x.IsEnabled,
                cancellationToken))
            return ValidationProblem("所选学期不存在或已停用。");
        plan.AcademicTermId = request.AcademicTermId;
        plan.Name = request.Name.Trim();
        plan.Version = request.Version.Trim();
        plan.Notes = Normalize(request.Notes);
        return await SaveAsync(id, false, cancellationToken);
    }

    [HttpPost("plans/{id:guid}/clone")]
    public async Task<ActionResult> ClonePlan(
        Guid id,
        CloneSchedulePlanRequest request,
        CancellationToken cancellationToken)
    {
        if (await HasActiveScheduleJobAsync(id, cancellationToken))
            return ScheduleJobRunningProblem();
        var source = await db.SchedulePlans.AsNoTracking()
            .Include(x => x.Entries)
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
        if (source is null) return NotFound();
        var copy = new SchedulePlan
        {
            AcademicTermId = source.AcademicTermId,
            Name = request.Name.Trim(),
            Version = request.Version.Trim(),
            Notes = source.Notes,
            Entries = source.Entries.Select(entry => new ScheduleEntry
            {
                TeachingTaskId = entry.TeachingTaskId,
                Kind = entry.Kind,
                ClassroomId = entry.ClassroomId,
                DayOfWeek = entry.DayOfWeek,
                StartPeriod = entry.StartPeriod,
                PeriodCount = entry.PeriodCount,
                StartWeek = entry.StartWeek,
                EndWeek = entry.EndWeek,
                WeekPattern = entry.WeekPattern,
                Notes = entry.Notes
            }).ToList()
        };
        db.SchedulePlans.Add(copy);
        return await SaveAsync(copy.Id, true, cancellationToken);
    }

    [HttpDelete("plans/{id:guid}")]
    public async Task<ActionResult> DeletePlan(Guid id, CancellationToken cancellationToken)
    {
        var plan = await db.SchedulePlans.FindAsync([id], cancellationToken);
        if (plan is null) return NotFound();
        if (await HasActiveScheduleJobAsync(id, cancellationToken))
            return ScheduleJobRunningProblem();
        if (plan.Status != SchedulePlanStatus.Draft)
            return ConflictProblem("仅草稿排课版本可以删除。");
        db.SchedulePlans.Remove(plan);
        return await SaveAsync(id, false, cancellationToken);
    }

    [HttpPost("plans/{id:guid}/publish")]
    public async Task<ActionResult<SchedulePublishJobResponse>> PublishPlan(
        Guid id,
        CancellationToken cancellationToken)
    {
        if (await HasActiveAutomaticScheduleJobAsync(id, cancellationToken))
            return AutomaticScheduleRunningProblem();
        var plan = await db.SchedulePlans.AsNoTracking()
            .Select(x => new
            {
                x.Id,
                x.AcademicTermId,
                x.Status,
                HasEntries = x.Entries.Any()
            })
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
        if (plan is null) return NotFound();
        if (plan.Status != SchedulePlanStatus.Draft)
            return ConflictProblem("只有草稿排课版本可以发布。");
        if (!plan.HasEntries)
            return ConflictProblem("排课版本中至少需要一条课表安排。");

        var existing = await db.SchedulePublishJobs.AsNoTracking()
            .FirstOrDefaultAsync(
                x => x.ActiveAcademicTermId == plan.AcademicTermId,
                cancellationToken);
        if (existing is not null)
        {
            if (existing.SchedulePlanId != id)
                return ConflictProblem(
                    "同一学期已有课表正在后台检查并发布，请等待任务完成。");
            return AcceptedAtAction(
                nameof(GetSchedulePublishJob),
                new { jobId = existing.Id },
                ToResponse(existing));
        }

        var requestedByUserId = CurrentUserId();
        var job = new SchedulePublishJob
        {
            SchedulePlanId = id,
            AcademicTermId = plan.AcademicTermId,
            ActiveAcademicTermId = plan.AcademicTermId,
            RequestedByUserId = requestedByUserId,
            CurrentStep = "等待后台检查"
        };
        db.SchedulePublishJobs.Add(job);
        db.BackgroundJobOutboxMessages.Add(
            BackgroundJobOutboxMessage.Create(
                BackgroundJobKind.SchedulePublish,
                job.Id));
        try
        {
            await db.SaveChangesAsync(cancellationToken);
        }
        catch (DbUpdateException)
        {
            db.Entry(job).State = EntityState.Detached;
            existing = await db.SchedulePublishJobs.AsNoTracking()
                .FirstOrDefaultAsync(
                    x => x.ActiveAcademicTermId == plan.AcademicTermId,
                    cancellationToken);
            if (existing is null)
                throw;
            if (existing.SchedulePlanId != id)
                return ConflictProblem(
                    "同一学期已有课表正在后台检查并发布，请等待任务完成。");
            return AcceptedAtAction(
                nameof(GetSchedulePublishJob),
                new { jobId = existing.Id },
                ToResponse(existing));
        }

        return AcceptedAtAction(
            nameof(GetSchedulePublishJob),
            new { jobId = job.Id },
            ToResponse(job));
    }

    [HttpPost("plans/{planId:guid}/entries")]
    public async Task<ActionResult> CreateEntry(
        Guid planId,
        ScheduleEntryRequest request,
        CancellationToken cancellationToken)
    {
        if (await HasActiveScheduleJobAsync(planId, cancellationToken))
            return ScheduleJobRunningProblem();
        var plan = await DraftPlanAsync(planId, cancellationToken);
        if (plan is null) return NotFound();
        var validation = await ValidateEntryAsync(plan, null, request, cancellationToken);
        if (validation is not null) return validation;
        var entry = CreateEntryEntity(planId, request);
        db.ScheduleEntries.Add(entry);
        return await SaveAsync(entry.Id, true, cancellationToken);
    }

    [HttpPost("plans/{planId:guid}/auto-schedule")]
    public async Task<ActionResult<AutomaticScheduleJobResponse>> AutoSchedule(
        Guid planId,
        CancellationToken cancellationToken)
    {
        var plan = await DraftPlanAsync(planId, cancellationToken);
        if (plan is null) return NotFound();
        if (await HasActiveSchedulePublishJobAsync(planId, cancellationToken))
            return SchedulePublishRunningProblem();

        var existing = await db.AutomaticScheduleJobs.AsNoTracking()
            .FirstOrDefaultAsync(
                x => x.ActiveSchedulePlanId == planId,
                cancellationToken);
        if (existing is not null)
        {
            return AcceptedAtAction(
                nameof(GetAutomaticScheduleJob),
                new { jobId = existing.Id },
                ToResponse(existing));
        }

        var requestedByUserId = Guid.TryParse(
            User.FindFirstValue(ClaimTypes.NameIdentifier),
            out var userId)
            ? userId
            : (Guid?)null;
        var job = new AutomaticScheduleJob
        {
            SchedulePlanId = planId,
            ActiveSchedulePlanId = planId,
            RequestedByUserId = requestedByUserId
        };
        db.AutomaticScheduleJobs.Add(job);
        db.BackgroundJobOutboxMessages.Add(
            BackgroundJobOutboxMessage.Create(
                BackgroundJobKind.AutomaticSchedule,
                job.Id));
        try
        {
            await db.SaveChangesAsync(cancellationToken);
        }
        catch (DbUpdateException)
        {
            db.Entry(job).State = EntityState.Detached;
            existing = await db.AutomaticScheduleJobs.AsNoTracking()
                .FirstOrDefaultAsync(
                    x => x.ActiveSchedulePlanId == planId,
                    cancellationToken);
            if (existing is null) throw;
            return AcceptedAtAction(
                nameof(GetAutomaticScheduleJob),
                new { jobId = existing.Id },
                ToResponse(existing));
        }

        return AcceptedAtAction(
            nameof(GetAutomaticScheduleJob),
            new { jobId = job.Id },
            ToResponse(job));
    }

    [HttpGet("plans/{planId:guid}/preflight")]
    public async Task<ActionResult> Preflight(Guid planId, CancellationToken cancellationToken)
    {
        var plan = await DraftPlanAsync(planId, cancellationToken);
        if (plan is null) return NotFound();
        var tasks = await db.TeachingTasks.AsNoTracking()
            .Where(x => x.AcademicTermId == plan.AcademicTermId &&
                        x.Status == TeachingTaskStatus.Published &&
                        x.SchedulingMode == TeachingTaskSchedulingMode.Standard)
            .Select(x => new { x.Id, x.Name, CourseName = x.Course!.Name })
            .ToListAsync(cancellationToken);
        var scheduled = await db.ScheduleEntries.AsNoTracking()
            .Where(x => x.SchedulePlanId == planId)
            .Select(x => x.TeachingTaskId).Distinct().ToListAsync(cancellationToken);
        var missing = tasks.Where(x => !scheduled.Contains(x.Id))
            .Select(x => $"《{x.CourseName}》{x.Name}").Take(20).ToList();
        return Ok(new { totalTasks = tasks.Count, scheduledTasks = scheduled.Count, unscheduledTasks = missing.Count, messages = missing });
    }

    [HttpGet("auto-schedule-jobs/{jobId:guid}")]
    public async Task<ActionResult<AutomaticScheduleJobResponse>>
        GetAutomaticScheduleJob(
            Guid jobId,
            CancellationToken cancellationToken)
    {
        var job = await db.AutomaticScheduleJobs.AsNoTracking()
            .FirstOrDefaultAsync(x => x.Id == jobId, cancellationToken);
        return job is null ? NotFound() : Ok(ToResponse(job));
    }

    [HttpGet("plans/{planId:guid}/auto-schedule-job")]
    public async Task<ActionResult<AutomaticScheduleJobResponse?>>
        GetLatestAutomaticScheduleJob(
            Guid planId,
        CancellationToken cancellationToken)
    {
        var job = await db.AutomaticScheduleJobs.AsNoTracking()
            .Where(x => x.SchedulePlanId == planId)
            .OrderByDescending(x => x.CreatedAt)
            .FirstOrDefaultAsync(cancellationToken);
        return Ok(job is null ? null : ToResponse(job));
    }

    [HttpGet("publish-jobs/{jobId:guid}")]
    public async Task<ActionResult<SchedulePublishJobResponse>>
        GetSchedulePublishJob(
            Guid jobId,
            CancellationToken cancellationToken)
    {
        var job = await db.SchedulePublishJobs.AsNoTracking()
            .FirstOrDefaultAsync(x => x.Id == jobId, cancellationToken);
        return job is null ? NotFound() : Ok(ToResponse(job));
    }

    [HttpGet("plans/{planId:guid}/publish-job")]
    public async Task<ActionResult<SchedulePublishJobResponse?>>
        GetLatestSchedulePublishJob(
            Guid planId,
            CancellationToken cancellationToken)
    {
        var job = await db.SchedulePublishJobs.AsNoTracking()
            .Where(x => x.SchedulePlanId == planId)
            .OrderByDescending(x => x.CreatedAt)
            .FirstOrDefaultAsync(cancellationToken);
        return Ok(job is null ? null : ToResponse(job));
    }

    [HttpPut("plans/{planId:guid}/entries/{entryId:guid}")]
    public async Task<ActionResult> UpdateEntry(
        Guid planId,
        Guid entryId,
        ScheduleEntryRequest request,
        CancellationToken cancellationToken)
    {
        if (await HasActiveScheduleJobAsync(planId, cancellationToken))
            return ScheduleJobRunningProblem();
        var plan = await DraftPlanAsync(planId, cancellationToken);
        if (plan is null) return NotFound();
        var entry = await db.ScheduleEntries
            .FirstOrDefaultAsync(
                x => x.Id == entryId && x.SchedulePlanId == planId,
                cancellationToken);
        if (entry is null) return NotFound();
        var validation = await ValidateEntryAsync(plan, entryId, request, cancellationToken);
        if (validation is not null) return validation;
        entry.TeachingTaskId = request.TeachingTaskId;
        entry.Kind = request.Kind;
        entry.ClassroomId = request.ClassroomId;
        entry.DayOfWeek = request.DayOfWeek;
        entry.StartPeriod = request.StartPeriod;
        entry.PeriodCount = request.PeriodCount;
        entry.StartWeek = request.StartWeek;
        entry.EndWeek = request.EndWeek;
        entry.WeekPattern = request.WeekPattern;
        entry.Notes = Normalize(request.Notes);
        return await SaveAsync(entryId, false, cancellationToken);
    }

    [HttpDelete("plans/{planId:guid}/entries/{entryId:guid}")]
    public async Task<ActionResult> DeleteEntry(
        Guid planId,
        Guid entryId,
        CancellationToken cancellationToken)
    {
        if (await HasActiveScheduleJobAsync(planId, cancellationToken))
            return ScheduleJobRunningProblem();
        if (await DraftPlanAsync(planId, cancellationToken) is null) return NotFound();
        var entry = await db.ScheduleEntries
            .FirstOrDefaultAsync(
                x => x.Id == entryId && x.SchedulePlanId == planId,
                cancellationToken);
        if (entry is null) return NotFound();
        db.ScheduleEntries.Remove(entry);
        return await SaveAsync(entryId, false, cancellationToken);
    }

    private async Task<SchedulePlan?> DraftPlanAsync(
        Guid id,
        CancellationToken cancellationToken) =>
        await db.SchedulePlans.FirstOrDefaultAsync(
            x => x.Id == id && x.Status == SchedulePlanStatus.Draft,
            cancellationToken);

    private Task<bool> HasActiveAutomaticScheduleJobAsync(
        Guid planId,
        CancellationToken cancellationToken) =>
        db.AutomaticScheduleJobs.AsNoTracking().AnyAsync(
            x => x.ActiveSchedulePlanId == planId,
            cancellationToken);

    private Task<bool> HasActiveSchedulePublishJobAsync(
        Guid planId,
        CancellationToken cancellationToken) =>
        db.SchedulePublishJobs.AsNoTracking().AnyAsync(
            x => x.SchedulePlanId == planId && x.ActiveAcademicTermId != null,
            cancellationToken);

    private async Task<bool> HasActiveScheduleJobAsync(
        Guid planId,
        CancellationToken cancellationToken) =>
        await HasActiveAutomaticScheduleJobAsync(planId, cancellationToken) ||
        await HasActiveSchedulePublishJobAsync(planId, cancellationToken);

    private async Task<ActionResult?> ValidateEntryAsync(
        SchedulePlan plan,
        Guid? entryId,
        ScheduleEntryRequest request,
        CancellationToken cancellationToken)
    {
        if (request.StartWeek > request.EndWeek)
            return ValidationProblem("开始周不能晚于结束周。");
        var activePeriods = await db.ScheduleTimeSlots.AsNoTracking()
            .Where(x => x.AcademicTermId == plan.AcademicTermId && x.IsEnabled)
            .Select(x => x.PeriodNumber)
            .ToListAsync(cancellationToken);
        if (activePeriods.Count == 0)
            return ValidationProblem("请先维护该学期的上课时间表。");
        if (Enumerable.Range(request.StartPeriod, request.PeriodCount)
            .Any(period => !activePeriods.Contains(period)))
            return ValidationProblem("所选节次包含未启用或不存在的上课时间。");

        var task = await db.TeachingTasks.AsNoTracking()
            .Include(x => x.Teachers)
            .Include(x => x.Classes)
            .ThenInclude(x => x.AdministrativeClass)
            .ThenInclude(x => x!.Students)
            .Include(x => x.Course)
            .FirstOrDefaultAsync(x => x.Id == request.TeachingTaskId, cancellationToken);
         if (task is null ||
             task.Status != TeachingTaskStatus.Published ||
             task.AcademicTermId != plan.AcademicTermId)
             return ValidationProblem("只能安排同一学期内已发布的教学任务。");
         if (task.SchedulingMode == TeachingTaskSchedulingMode.Flexible)
             return ValidationProblem("非排时课程不进入正常课表，无需设置星期、节次或教室。");
        if (request.StartWeek < task.StartWeek || request.EndWeek > task.EndWeek)
            return ValidationProblem("排课周次必须位于教学任务的授课周次内。");
        var targetHours = TeachingTaskHours.TargetHours(
            task.Course!,
            request.Kind);
        if (targetHours == 0)
            return ValidationProblem(
                request.Kind == ScheduleEntryKind.Experiment
                    ? "该课程没有实践学时，不能安排实验课。"
                    : "该课程没有理论学时，不能安排理论课。");
        var existingEntries = await db.ScheduleEntries.AsNoTracking()
            .Where(x =>
                x.SchedulePlanId == plan.Id &&
                x.TeachingTaskId == request.TeachingTaskId &&
                x.Kind == request.Kind &&
                x.Id != entryId)
            .Select(x => new
            {
                x.StartWeek,
                x.EndWeek,
                x.WeekPattern,
                x.PeriodCount
            })
            .ToListAsync(cancellationToken);
        var existingHours = existingEntries.Sum(x =>
            TeachingTaskHours.ScheduledHours(
                x.StartWeek,
                x.EndWeek,
                x.WeekPattern,
                x.PeriodCount));
        var proposedHours = TeachingTaskHours.ScheduledHours(
            request.StartWeek,
            request.EndWeek,
            request.WeekPattern,
            request.PeriodCount);
        if (existingHours + proposedHours > targetHours)
            return ValidationProblem(
                $"该教学任务{(request.Kind == ScheduleEntryKind.Experiment ? "实验" : "理论")}课" +
                $"共需 {targetHours} 学时；当前操作后将达到 " +
                $"{existingHours + proposedHours} 学时。");

        var constraint = await db.TeachingTaskScheduleConstraints.AsNoTracking()
            .Include(x => x.AllowedClassrooms)
            .Include(x => x.AllowedExperimentClassrooms)
            .FirstOrDefaultAsync(
                x => x.TeachingTaskId == request.TeachingTaskId,
                cancellationToken);
        var requiresClassroom = request.Kind == ScheduleEntryKind.Experiment ||
                                constraint?.RequiresClassroom != false;
        if (requiresClassroom && !request.ClassroomId.HasValue)
            return ValidationProblem("该课程需要占用教室，请选择教室。");
        if (!requiresClassroom && request.ClassroomId.HasValue)
            return ValidationProblem("该课程已设置为不占用教室，请清空教室。");
        var allowedDays = ParseDays(constraint?.AllowedDayOfWeeks);
        if (allowedDays.Count > 0 && !allowedDays.Contains(request.DayOfWeek))
            return ValidationProblem("所选星期不在该教学任务允许的上课日内。");
        if (constraint?.EarliestPeriod is int earliest &&
            request.StartPeriod < earliest)
            return ValidationProblem($"该教学任务最早只能从第 {earliest} 节开始。");
        if (constraint?.LatestPeriod is int latest &&
            request.StartPeriod + request.PeriodCount - 1 > latest)
            return ValidationProblem($"该教学任务最晚必须在第 {latest} 节结束。");

        Classroom? classroom = null;
        if (request.ClassroomId.HasValue)
        {
            classroom = await db.Classrooms.AsNoTracking()
                .Include(x => x.Building)
                .FirstOrDefaultAsync(
                    x => x.Id == request.ClassroomId && x.IsEnabled,
                    cancellationToken);
            if (classroom is null) return ValidationProblem("所选教室不存在或已停用。");
            if (request.Kind != ScheduleEntryKind.Experiment &&
                constraint?.RequiredCampusId is Guid campusId &&
                classroom.Building!.CampusId != campusId)
                return ValidationProblem("所选教室不在该课程指定的校区。");
            if (request.Kind != ScheduleEntryKind.Experiment &&
                constraint?.RequiredBuildingId is Guid buildingId &&
                classroom.BuildingId != buildingId)
                return ValidationProblem("所选教室不在该课程指定的教学楼。");
            if (request.Kind == ScheduleEntryKind.Experiment &&
                constraint?.ExperimentRequiredCampusId is Guid experimentCampusId &&
                classroom.Building!.CampusId != experimentCampusId)
                return ValidationProblem("所选场地不在该实验课指定的校区。");
            if (request.Kind == ScheduleEntryKind.Experiment &&
                constraint?.ExperimentRequiredBuildingId is Guid experimentBuildingId &&
                classroom.BuildingId != experimentBuildingId)
                return ValidationProblem("所选场地不在该实验课指定的教学楼。");
            var allowedClassroomIds = constraint?.AllowedClassrooms
                .Select(x => x.ClassroomId)
                .ToHashSet() ?? [];
            if (request.Kind != ScheduleEntryKind.Experiment && allowedClassroomIds.Count > 0 &&
                !allowedClassroomIds.Contains(classroom.Id))
                return ValidationProblem("所选教室不在该课程指定的教室范围内。");
            if (request.Kind == ScheduleEntryKind.Experiment &&
                constraint?.AllowedExperimentVenueNatures is { } allowedNatures &&
                allowedNatures != 0 &&
                (classroom.TeachingVenueNature & allowedNatures) == 0)
                return ValidationProblem("所选场地不在该实验课允许的教学场地性质范围内。");
            var allowedExperimentClassroomIds = constraint?.AllowedExperimentClassrooms
                .Select(x => x.ClassroomId)
                .ToHashSet() ?? [];
            if (request.Kind == ScheduleEntryKind.Experiment &&
                allowedExperimentClassroomIds.Count > 0 &&
                !allowedExperimentClassroomIds.Contains(classroom.Id))
                return ValidationProblem("所选场地不在该实验课指定的场地范围内。");
        }
        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)
            return ConflictProblem(
                $"教室容量不足：教学任务容量为 {requiredCapacity} 人，教室仅容纳 {classroom.Capacity} 人。");

        var candidates = await db.ScheduleEntries.AsNoTracking()
            .Where(x =>
                x.SchedulePlanId == plan.Id &&
                x.Id != entryId &&
                x.DayOfWeek == request.DayOfWeek &&
                x.StartWeek <= request.EndWeek &&
                x.EndWeek >= request.StartWeek &&
                x.StartPeriod < request.StartPeriod + request.PeriodCount &&
                request.StartPeriod < x.StartPeriod + x.PeriodCount)
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.Teachers)
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.Classes)
            .ToListAsync(cancellationToken);
        var proposed = CreateEntryEntity(plan.Id, request);
        proposed.TeachingTask = task;
        foreach (var candidate in candidates)
        {
            if (!ScheduleConflictDetector.TimeOverlaps(candidate, proposed)) continue;
            var reason = ScheduleConflictDetector.ConflictReason(candidate, proposed);
            if (reason is not null)
                return ConflictProblem(
                    $"与“{candidate.TeachingTask!.Name}”发生{reason}冲突。");
        }
        return null;
    }

    private static ScheduleEntry CreateEntryEntity(Guid planId, ScheduleEntryRequest request) =>
        new()
        {
            SchedulePlanId = planId,
            TeachingTaskId = request.TeachingTaskId,
            Kind = request.Kind,
            ClassroomId = request.ClassroomId,
            DayOfWeek = request.DayOfWeek,
            StartPeriod = request.StartPeriod,
            PeriodCount = request.PeriodCount,
            StartWeek = request.StartWeek,
            EndWeek = request.EndWeek,
            WeekPattern = request.WeekPattern,
            Notes = Normalize(request.Notes)
        };

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


    private async Task<ActionResult> SaveAsync(
        Guid id,
        bool created,
        CancellationToken cancellationToken)
    {
        try
        {
            await db.SaveChangesAsync(cancellationToken);
            return created ? Created(string.Empty, new { id }) : NoContent();
        }
        catch (DbUpdateException)
        {
            return ConflictProblem("排课版本重复，或关联数据已失效。");
        }
    }

    private ActionResult ConflictProblem(string detail) =>
        Conflict(new ProblemDetails
        {
            Title = "排课冲突",
            Detail = detail,
            Status = StatusCodes.Status409Conflict
        });

    private ActionResult AutomaticScheduleRunningProblem() =>
        ConflictProblem("自动排课正在后台运行，请等待任务完成后再修改该排课版本。");

    private ActionResult SchedulePublishRunningProblem() =>
        ConflictProblem("课表正在后台检查并发布，请等待任务完成后再修改。");

    private ActionResult ScheduleJobRunningProblem() =>
        ConflictProblem("后台任务正在处理该排课版本，请等待任务完成后再修改。");

    private static AutomaticScheduleJobResponse ToResponse(AutomaticScheduleJob job)
    {
        IReadOnlyList<string> messages = [];
        if (!string.IsNullOrWhiteSpace(job.MessagesJson))
        {
            messages = JsonSerializer.Deserialize<string[]>(job.MessagesJson) ?? [];
        }

        return new(
            job.Id,
            job.SchedulePlanId,
            job.Status,
            job.TotalTasks,
            job.ProcessedTasks,
            job.CreatedEntries,
            job.CompletedTasks,
            messages,
            job.ErrorMessage,
            job.CreatedAt,
            job.StartedAt,
            job.CompletedAt);
    }

    private static SchedulePublishJobResponse ToResponse(SchedulePublishJob job) =>
        new(
            job.Id,
            job.SchedulePlanId,
            job.Status,
            job.TotalSteps,
            job.CompletedSteps,
            job.CurrentStep,
            job.ErrorMessage,
            job.CreatedAt,
            job.StartedAt,
            job.CompletedAt);

    private Guid? CurrentUserId() =>
        Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId)
            ? userId
            : null;

    private static string? Normalize(string? value) =>
        string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}

public sealed record SchedulePlanRequest(
    Guid AcademicTermId,
    [Required, MaxLength(120)] string Name,
    [Required, MaxLength(30)] string Version,
    [MaxLength(500)] string? Notes);

public sealed record CloneSchedulePlanRequest(
    [Required, MaxLength(120)] string Name,
    [Required, MaxLength(30)] string Version);

public sealed record ScheduleEntryRequest(
    Guid TeachingTaskId,
    Guid? ClassroomId,
    [Range(1, 7)] int DayOfWeek,
    [Range(1, 30)] int StartPeriod,
    [Range(1, 6)] int PeriodCount,
    [Range(1, 30)] int StartWeek,
    [Range(1, 30)] int EndWeek,
    WeekPattern WeekPattern,
    [MaxLength(500)] string? Notes,
    ScheduleEntryKind Kind = ScheduleEntryKind.Lecture);

public sealed record AutomaticScheduleJobResponse(
    Guid Id,
    Guid SchedulePlanId,
    AutomaticScheduleJobStatus Status,
    int TotalTasks,
    int ProcessedTasks,
    int CreatedEntries,
    int CompletedTasks,
    IReadOnlyList<string> Messages,
    string? ErrorMessage,
    DateTime CreatedAt,
    DateTime? StartedAt,
    DateTime? CompletedAt);

public sealed record SchedulePublishJobResponse(
    Guid Id,
    Guid SchedulePlanId,
    SchedulePublishJobStatus Status,
    int TotalSteps,
    int CompletedSteps,
    string? CurrentStep,
    string? ErrorMessage,
    DateTime CreatedAt,
    DateTime? StartedAt,
    DateTime? CompletedAt);
