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

namespace Jiaowu.Api.Controllers;

[ApiController]
[Authorize(Roles = SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin)]
[Route("api/schedules")]
public sealed class ScheduleSettingsController(AppDbContext db, IAppCache cache) : ControllerBase
{
    [HttpGet("time-slots")]
    public async Task<ActionResult> GetTimeSlots(
        Guid academicTermId,
        CancellationToken cancellationToken) =>
        Ok(await db.ScheduleTimeSlots.AsNoTracking()
            .Where(x => x.AcademicTermId == academicTermId)
            .OrderBy(x => x.PeriodNumber)
            .Select(x => new
            {
                x.Id,
                x.PeriodNumber,
                x.Name,
                StartsAt = x.StartsAt.ToString("HH:mm"),
                EndsAt = x.EndsAt.ToString("HH:mm"),
                x.IsEnabled
            })
            .ToListAsync(cancellationToken));

    [HttpPut("time-slots/{academicTermId:guid}")]
    public async Task<ActionResult> ReplaceTimeSlots(
        Guid academicTermId,
        IReadOnlyList<ScheduleTimeSlotRequest> requests,
        CancellationToken cancellationToken)
    {
        if (!await db.AcademicTerms.AnyAsync(
                x => x.Id == academicTermId && x.IsEnabled,
                cancellationToken))
            return ValidationProblem("学期不存在或已停用。");
        if (requests.Count == 0)
            return ValidationProblem("上课时间表至少需要一个节次。");
        if (requests.Select(x => x.PeriodNumber).Distinct().Count() != requests.Count)
            return ValidationProblem("节次编号不能重复。");
        if (requests.Any(x => x.StartsAt >= x.EndsAt))
            return ValidationProblem("每个节次的上课时间必须早于下课时间。");
        var ordered = requests.OrderBy(x => x.StartsAt).ToList();
        if (ordered.Zip(ordered.Skip(1)).Any(pair => pair.First.EndsAt > pair.Second.StartsAt))
            return ValidationProblem("上课时间段不能相互重叠。");

        var existing = await db.ScheduleTimeSlots
            .Where(x => x.AcademicTermId == academicTermId)
            .ToListAsync(cancellationToken);
        db.ScheduleTimeSlots.RemoveRange(existing);
        db.ScheduleTimeSlots.AddRange(requests.Select(request => new ScheduleTimeSlot
        {
            AcademicTermId = academicTermId,
            PeriodNumber = request.PeriodNumber,
            Name = request.Name.Trim(),
            StartsAt = request.StartsAt,
            EndsAt = request.EndsAt,
            IsEnabled = request.IsEnabled
        }));
        await db.SaveChangesAsync(cancellationToken);
        await cache.RemoveByTagAsync(AppCacheTags.Timetables, cancellationToken);
        return NoContent();
    }

    [HttpGet("constraints")]
    public async Task<ActionResult> GetConstraints(
        Guid academicTermId,
        CancellationToken cancellationToken)
    {
        var tasks = await db.TeachingTasks.AsNoTracking()
            .Where(x =>
                x.AcademicTermId == academicTermId &&
                x.Status == TeachingTaskStatus.Published)
            .OrderBy(x => x.TaskNumber)
            .Select(x => new
            {
                x.Id,
                x.TaskNumber,
                x.Name,
                CourseCode = x.Course!.Code,
                CourseName = x.Course!.Name,
                CollegeId = x.Course.CollegeId,
                CollegeName = x.Course.College!.Name,
                TeacherNames = x.Teachers
                    .OrderByDescending(item => item.IsPrimary)
                    .Select(item => item.Teacher!.Name),
                x.Capacity,
                x.StartWeek,
                x.EndWeek,
                x.WeeklyHours,
                CourseTotalHours = x.Course.TotalHours,
                CoursePracticeHours = x.Course.PracticeHours,
                x.SchedulingMode
            })
            .ToListAsync(cancellationToken);
        var taskIds = tasks.Select(x => x.Id).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);
        return Ok(tasks.Select(task =>
        {
            constraints.TryGetValue(task.Id, out var constraint);
            return new
            {
                task.Id,
                task.TaskNumber,
                task.Name,
                task.CourseCode,
                task.CourseName,
                task.CollegeId,
                task.CollegeName,
                task.TeacherNames,
                task.Capacity,
                task.StartWeek,
                task.EndWeek,
                task.WeeklyHours,
                task.CourseTotalHours,
                task.CoursePracticeHours,
                task.SchedulingMode,
                HasCustomConstraint = constraint is not null,
                RequiresClassroom = task.SchedulingMode == TeachingTaskSchedulingMode.Flexible
                     ? false
                     : constraint?.RequiresClassroom ?? true,
                constraint?.RequiredCampusId,
                constraint?.RequiredBuildingId,
                constraint?.ExperimentRequiredCampusId,
                constraint?.ExperimentRequiredBuildingId,
                AllowedDayOfWeeks = ParseDays(constraint?.AllowedDayOfWeeks),
                constraint?.EarliestPeriod,
                constraint?.LatestPeriod,
                AllowedClassroomIds = constraint?.AllowedClassrooms
                    .Select(x => x.ClassroomId) ?? [],
                AllowedExperimentClassroomIds = constraint?.AllowedExperimentClassrooms
                    .Select(x => x.ClassroomId) ?? [],
                AllowedExperimentVenueNatures = constraint?.AllowedExperimentVenueNatures ?? 0
            };
        }));
    }

    [HttpPut("constraints/{teachingTaskId:guid}")]
    public async Task<ActionResult> SaveConstraint(
        Guid teachingTaskId,
        TeachingTaskScheduleConstraintRequest request,
        CancellationToken cancellationToken)
    {
        if (request.AllowedDayOfWeeks.Any(day => day is < 1 or > 7))
            return ValidationProblem("允许上课日必须位于星期一至星期日。");
        if (request.EarliestPeriod.HasValue &&
            request.LatestPeriod.HasValue &&
            request.EarliestPeriod > request.LatestPeriod)
            return ValidationProblem("最早节次不能晚于最晚节次。");
        if (!Enum.IsDefined(request.SchedulingMode))
            return ValidationProblem("授课方式无效。");
        var task = await db.TeachingTasks
            .FirstOrDefaultAsync(x => x.Id == teachingTaskId, cancellationToken);
        if (task is null) return NotFound();
        if (request.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
            await HasScheduleEntriesAsync([teachingTaskId], cancellationToken))
            return ConflictProblem("该教学任务已有正常排课记录，请先删除排课记录后再改为非排时课程。");

        task.SchedulingMode = request.SchedulingMode;
        if (request.SchedulingMode == TeachingTaskSchedulingMode.Flexible)
        {
            var flexibleConstraint = await db.TeachingTaskScheduleConstraints
                .Include(x => x.AllowedClassrooms)
                .Include(x => x.AllowedExperimentClassrooms)
                .FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken);
            if (flexibleConstraint is not null)
            {
                ClearConstraint(flexibleConstraint);
            }
            await db.SaveChangesAsync(cancellationToken);
            return NoContent();
        }

        Building? building = null;
        if (request.RequiredBuildingId.HasValue)
        {
            building = await db.Buildings.AsNoTracking()
                .FirstOrDefaultAsync(
                    x => x.Id == request.RequiredBuildingId && x.IsEnabled,
                    cancellationToken);
            if (building is null) return ValidationProblem("指定教学楼不存在或已停用。");
            if (request.RequiredCampusId.HasValue &&
                building.CampusId != request.RequiredCampusId)
                return ValidationProblem("指定教学楼不属于所选校区。");
        }
        if (request.RequiredCampusId.HasValue &&
            !await db.Campuses.AnyAsync(
                x => x.Id == request.RequiredCampusId && x.IsEnabled,
                cancellationToken))
            return ValidationProblem("指定校区不存在或已停用。");

        Building? experimentBuilding = null;
        if (request.ExperimentRequiredBuildingId.HasValue)
        {
            experimentBuilding = await db.Buildings.AsNoTracking()
                .FirstOrDefaultAsync(x => x.Id == request.ExperimentRequiredBuildingId && x.IsEnabled,
                    cancellationToken);
            if (experimentBuilding is null) return ValidationProblem("指定实验教学楼不存在或已停用。");
            if (request.ExperimentRequiredCampusId.HasValue &&
                experimentBuilding.CampusId != request.ExperimentRequiredCampusId)
                return ValidationProblem("指定实验教学楼不属于所选校区。");
        }
        if (request.ExperimentRequiredCampusId.HasValue &&
            !await db.Campuses.AnyAsync(x => x.Id == request.ExperimentRequiredCampusId && x.IsEnabled,
                cancellationToken))
            return ValidationProblem("指定实验校区不存在或已停用。");

        var allowedRooms = await db.Classrooms.AsNoTracking()
            .Where(x => x.IsEnabled)
            .WhereIn(request.AllowedClassroomIds, x => x.Id)
            .Include(x => x.Building)
            .ToListAsync(cancellationToken);
        if (allowedRooms.Count != request.AllowedClassroomIds.Distinct().Count())
            return ValidationProblem("部分指定教室不存在或已停用。");
        if (building is not null && allowedRooms.Any(x => x.BuildingId != building.Id))
            return ValidationProblem("指定教室必须位于所选教学楼。");
        if (request.RequiredCampusId.HasValue &&
            allowedRooms.Any(x => x.Building!.CampusId != request.RequiredCampusId))
            return ValidationProblem("指定教室必须位于所选校区。");

        var allowedExperimentRoomIds = request.AllowedExperimentClassroomIds?.Distinct().ToArray() ?? [];
        var allowedExperimentRooms = await db.Classrooms.AsNoTracking()
            .Where(x => x.IsEnabled)
            .WhereIn(allowedExperimentRoomIds, x => x.Id)
            .Include(x => x.Building)
            .ToListAsync(cancellationToken);
        if (allowedExperimentRooms.Count != allowedExperimentRoomIds.Length)
            return ValidationProblem("部分指定实验场地不存在或已停用。");
        if (experimentBuilding is not null && allowedExperimentRooms.Any(x => x.BuildingId != experimentBuilding.Id))
            return ValidationProblem("指定实验场地必须位于所选实验教学楼。");
        if (request.ExperimentRequiredCampusId.HasValue &&
            allowedExperimentRooms.Any(x => x.Building!.CampusId != request.ExperimentRequiredCampusId))
            return ValidationProblem("指定实验场地必须位于所选实验校区。");

        var constraint = await db.TeachingTaskScheduleConstraints
            .Include(x => x.AllowedClassrooms)
            .Include(x => x.AllowedExperimentClassrooms)
            .FirstOrDefaultAsync(x => x.TeachingTaskId == teachingTaskId, cancellationToken);
        if (constraint is null)
        {
            constraint = new TeachingTaskScheduleConstraint { TeachingTaskId = teachingTaskId };
            db.TeachingTaskScheduleConstraints.Add(constraint);
        }
        constraint.RequiresClassroom = request.RequiresClassroom;
        constraint.RequiredCampusId = request.RequiresClassroom
            ? request.RequiredCampusId
            : null;
        constraint.RequiredBuildingId = request.RequiresClassroom
            ? request.RequiredBuildingId
            : null;
        constraint.ExperimentRequiredCampusId = request.RequiresClassroom
            ? request.ExperimentRequiredCampusId
            : null;
        constraint.ExperimentRequiredBuildingId = request.RequiresClassroom
            ? request.ExperimentRequiredBuildingId
            : null;
        constraint.AllowedDayOfWeeks = request.AllowedDayOfWeeks.Count == 0
            ? null
            : string.Join(',', request.AllowedDayOfWeeks.Distinct().Order());
        constraint.EarliestPeriod = request.EarliestPeriod;
        constraint.LatestPeriod = request.LatestPeriod;
        constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures;
        db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
        db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(constraint.AllowedExperimentClassrooms);
        constraint.AllowedClassrooms = request.RequiresClassroom
            ? request.AllowedClassroomIds.Distinct().Select(classroomId =>
                new TeachingTaskAllowedClassroom { ClassroomId = classroomId }).ToList()
            : [];
        constraint.AllowedExperimentClassrooms = request.RequiresClassroom
            ? allowedExperimentRoomIds.Select(classroomId =>
                new TeachingTaskAllowedExperimentClassroom { ClassroomId = classroomId }).ToList()
            : [];
        await db.SaveChangesAsync(cancellationToken);
        return NoContent();
    }

    [HttpPut("constraints/batch")]
    public async Task<ActionResult> SaveConstraintsBatch(
        TeachingTaskScheduleConstraintBatchRequest request,
        CancellationToken cancellationToken)
    {
        var taskIds = request.TeachingTaskIds.Distinct().ToArray();
        if (taskIds.Length == 0)
            return ValidationProblem("请至少选择一个教学任务。");
        if (request.AllowedDayOfWeeks?.Any(day => day is < 1 or > 7) == true)
            return ValidationProblem("允许上课日必须位于星期一至星期日。");
        if (request.EarliestPeriod.HasValue &&
            request.LatestPeriod.HasValue &&
            request.EarliestPeriod > request.LatestPeriod)
            return ValidationProblem("最早节次不能晚于最晚节次。");
        if (request.SchedulingMode.HasValue &&
            !Enum.IsDefined(request.SchedulingMode.Value))
            return ValidationProblem("授课方式无效。");
        if (!request.SchedulingMode.HasValue &&
             !request.RequiresClassroom.HasValue &&
             request.AllowedDayOfWeeks is null &&
             !request.UpdateClassroomScope &&
             !request.UpdateExperimentClassroomScope &&
             !request.AllowedExperimentVenueNatures.HasValue &&
             !request.UpdatePeriodRange &&
            !request.EarliestPeriod.HasValue &&
            !request.LatestPeriod.HasValue)
            return ValidationProblem("请至少选择一项需要批量修改的设置。");
        if (request.UpdateClassroomScope && request.RequiresClassroom == false)
            return ValidationProblem("批量指定教室范围时，场地要求不能设置为不占用教室。");
        if (request.UpdateExperimentClassroomScope && request.RequiresClassroom == false)
            return ValidationProblem("批量指定实验场地时，场地要求不能设置为不占用教室。");

        var tasks = await db.TeachingTasks
            .Where(x =>
                x.AcademicTermId == request.AcademicTermId &&
                x.Status == TeachingTaskStatus.Published)
            .WhereIn(taskIds, x => x.Id)
            .Include(x => x.Course)
            .ToListAsync(cancellationToken);
        if (tasks.Count != taskIds.Length)
            return ValidationProblem("部分教学任务不存在、未发布或不属于当前学期。");
        if (request.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
            await HasScheduleEntriesAsync(taskIds, cancellationToken))
            return ConflictProblem("所选教学任务中存在已有正常排课记录的课程，请先删除这些排课记录。");
        if (request.UpdateClassroomScope && tasks.Any(task =>
                (request.SchedulingMode ?? task.SchedulingMode) ==
                TeachingTaskSchedulingMode.Flexible))
            return ConflictProblem("非排时课程不能指定教室，请先将当前筛选结果限定为正常排课课程。");
        if (request.UpdateExperimentClassroomScope && tasks.Any(task =>
                (request.SchedulingMode ?? task.SchedulingMode) ==
                TeachingTaskSchedulingMode.Flexible))
            return ConflictProblem("非排时课程不能指定实验场地，请先将当前筛选结果限定为正常排课课程。");

        Building? building = null;
        List<Classroom> allowedRooms = [];
        var experimentRoomIds = request.AllowedExperimentClassroomIds?.Distinct().ToArray() ?? [];
        Building? experimentBuilding = null;
        List<Classroom> allowedExperimentRooms = [];
        if (request.UpdateClassroomScope)
        {
            if (request.RequiredBuildingId.HasValue)
            {
                building = await db.Buildings.AsNoTracking()
                    .FirstOrDefaultAsync(
                        x => x.Id == request.RequiredBuildingId && x.IsEnabled,
                        cancellationToken);
                if (building is null)
                    return ValidationProblem("指定教学楼不存在或已停用。");
                if (request.RequiredCampusId.HasValue &&
                    building.CampusId != request.RequiredCampusId)
                    return ValidationProblem("指定教学楼不属于所选校区。");
            }
            if (request.RequiredCampusId.HasValue &&
                !await db.Campuses.AnyAsync(
                    x => x.Id == request.RequiredCampusId && x.IsEnabled,
                    cancellationToken))
                return ValidationProblem("指定校区不存在或已停用。");

            var roomIds = request.AllowedClassroomIds?.Distinct().ToArray() ?? [];
            allowedRooms = await db.Classrooms.AsNoTracking()
                .Where(x => x.IsEnabled)
                .WhereIn(roomIds, x => x.Id)
                .Include(x => x.Building)
                .ToListAsync(cancellationToken);
            if (allowedRooms.Count != roomIds.Length)
                return ValidationProblem("部分指定教室不存在或已停用。");
            if (building is not null &&
                allowedRooms.Any(x => x.BuildingId != building.Id))
                return ValidationProblem("指定教室必须位于所选教学楼。");
            if (request.RequiredCampusId.HasValue &&
                allowedRooms.Any(x =>
                    x.Building!.CampusId != request.RequiredCampusId))
                return ValidationProblem("指定教室必须位于所选校区。");
        }
        if (request.UpdateExperimentClassroomScope)
        {
            if (request.ExperimentRequiredBuildingId.HasValue)
            {
                experimentBuilding = await db.Buildings.AsNoTracking()
                    .FirstOrDefaultAsync(x => x.Id == request.ExperimentRequiredBuildingId && x.IsEnabled,
                        cancellationToken);
                if (experimentBuilding is null)
                    return ValidationProblem("指定实验教学楼不存在或已停用。");
                if (request.ExperimentRequiredCampusId.HasValue &&
                    experimentBuilding.CampusId != request.ExperimentRequiredCampusId)
                    return ValidationProblem("指定实验教学楼不属于所选实验校区。");
            }
            if (request.ExperimentRequiredCampusId.HasValue &&
                !await db.Campuses.AnyAsync(x => x.Id == request.ExperimentRequiredCampusId && x.IsEnabled,
                    cancellationToken))
                return ValidationProblem("指定实验校区不存在或已停用。");
            allowedExperimentRooms = await db.Classrooms.AsNoTracking()
                .Where(x => x.IsEnabled)
                .WhereIn(experimentRoomIds, x => x.Id)
                .Include(x => x.Building)
                .ToListAsync(cancellationToken);
            if (allowedExperimentRooms.Count != experimentRoomIds.Length)
                return ValidationProblem("部分指定实验场地不存在或已停用。");
            if (experimentBuilding is not null &&
                allowedExperimentRooms.Any(x => x.BuildingId != experimentBuilding.Id))
                return ValidationProblem("指定实验场地必须位于所选实验教学楼。");
            if (request.ExperimentRequiredCampusId.HasValue &&
                allowedExperimentRooms.Any(x => x.Building!.CampusId != request.ExperimentRequiredCampusId))
                return ValidationProblem("指定实验场地必须位于所选实验校区。");
        }

        var constraints = await db.TeachingTaskScheduleConstraints
           .WhereIn(taskIds, x => x.TeachingTaskId)
            .Include(x => x.AllowedClassrooms)
            .Include(x => x.AllowedExperimentClassrooms)
            .ToDictionaryAsync(x => x.TeachingTaskId, cancellationToken);
        foreach (var task in tasks)
        {
            if (request.SchedulingMode.HasValue)
                task.SchedulingMode = request.SchedulingMode.Value;
            if (task.SchedulingMode == TeachingTaskSchedulingMode.Flexible)
            {
                if (constraints.TryGetValue(task.Id, out var flexibleConstraint))
                    ClearConstraint(flexibleConstraint);
                continue;
            }

            if (!constraints.TryGetValue(task.Id, out var constraint))
            {
                var changesConstraint =
                    request.RequiresClassroom.HasValue ||
                    request.AllowedDayOfWeeks is not null ||
                    request.UpdateClassroomScope ||
                    request.UpdateExperimentClassroomScope ||
                    request.AllowedExperimentVenueNatures.HasValue ||
                    request.UpdatePeriodRange;
                if (!changesConstraint) continue;
                constraint = new TeachingTaskScheduleConstraint { TeachingTaskId = task.Id };
                constraints[task.Id] = constraint;
                db.TeachingTaskScheduleConstraints.Add(constraint);
            }

            if (request.RequiresClassroom.HasValue)
            {
                constraint.RequiresClassroom = request.RequiresClassroom.Value;
                if (!request.RequiresClassroom.Value)
                {
                    constraint.RequiredCampusId = null;
                    constraint.RequiredBuildingId = null;
                    db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
                    db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(
                        constraint.AllowedExperimentClassrooms);
                    constraint.AllowedClassrooms = [];
                    constraint.AllowedExperimentClassrooms = [];
                }
            }
            if (request.AllowedDayOfWeeks is not null)
            {
                constraint.AllowedDayOfWeeks = request.AllowedDayOfWeeks.Count == 0
                    ? null
                    : string.Join(',', request.AllowedDayOfWeeks.Distinct().Order());
            }
            if (request.UpdateClassroomScope)
            {
                constraint.RequiresClassroom = true;
                constraint.RequiredCampusId = request.RequiredCampusId;
                constraint.RequiredBuildingId = request.RequiredBuildingId;
                db.TeachingTaskAllowedClassrooms.RemoveRange(
                    constraint.AllowedClassrooms);
                constraint.AllowedClassrooms = allowedRooms.Select(room =>
                    new TeachingTaskAllowedClassroom
                    {
                        ClassroomId = room.Id
                    }).ToList();
            }
            if (task.Course?.PracticeHours > 0 && request.AllowedExperimentVenueNatures.HasValue)
                constraint.AllowedExperimentVenueNatures = request.AllowedExperimentVenueNatures.Value;
            if (task.Course?.PracticeHours > 0 && request.UpdateExperimentClassroomScope)
            {
                constraint.ExperimentRequiredCampusId = request.ExperimentRequiredCampusId;
                constraint.ExperimentRequiredBuildingId = request.ExperimentRequiredBuildingId;
                db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(
                    constraint.AllowedExperimentClassrooms);
                constraint.AllowedExperimentClassrooms = allowedExperimentRooms.Select(room =>
                    new TeachingTaskAllowedExperimentClassroom { ClassroomId = room.Id }).ToList();
            }
            if (request.UpdatePeriodRange)
            {
                constraint.EarliestPeriod = request.EarliestPeriod;
                constraint.LatestPeriod = request.LatestPeriod;
            }
        }

        await db.SaveChangesAsync(cancellationToken);
        return Ok(new { AffectedCount = tasks.Count });
    }

    private Task<bool> HasScheduleEntriesAsync(
        IReadOnlyCollection<Guid> taskIds,
        CancellationToken cancellationToken) =>
        db.ScheduleEntries.AsNoTracking()
            .WhereIn(taskIds, x => x.TeachingTaskId)
            .AnyAsync(cancellationToken);

    private void ClearConstraint(TeachingTaskScheduleConstraint constraint)
    {
        constraint.RequiresClassroom = false;
        constraint.RequiredCampusId = null;
        constraint.RequiredBuildingId = null;
        constraint.ExperimentRequiredCampusId = null;
        constraint.ExperimentRequiredBuildingId = null;
        constraint.AllowedDayOfWeeks = null;
        constraint.EarliestPeriod = null;
        constraint.LatestPeriod = null;
        db.TeachingTaskAllowedClassrooms.RemoveRange(constraint.AllowedClassrooms);
        db.TeachingTaskAllowedExperimentClassrooms.RemoveRange(constraint.AllowedExperimentClassrooms);
        constraint.AllowedClassrooms = [];
        constraint.AllowedExperimentClassrooms = [];
    }

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

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

public sealed record ScheduleTimeSlotRequest(
    [Range(1, 30)] int PeriodNumber,
    [Required, MaxLength(40)] string Name,
    TimeOnly StartsAt,
    TimeOnly EndsAt,
    bool IsEnabled);

public sealed record TeachingTaskScheduleConstraintRequest(
    TeachingTaskSchedulingMode SchedulingMode,
    bool RequiresClassroom,
    Guid? RequiredCampusId,
    Guid? RequiredBuildingId,
    IReadOnlyList<Guid> AllowedClassroomIds,
    IReadOnlyList<int> AllowedDayOfWeeks,
    [Range(1, 30)] int? EarliestPeriod,
    [Range(1, 30)] int? LatestPeriod,
    TeachingVenueNature AllowedExperimentVenueNatures = 0,
    IReadOnlyList<Guid>? AllowedExperimentClassroomIds = null,
    Guid? ExperimentRequiredCampusId = null,
    Guid? ExperimentRequiredBuildingId = null);

public sealed record TeachingTaskScheduleConstraintBatchRequest(
    Guid AcademicTermId,
    [MinLength(1), MaxLength(500)] IReadOnlyCollection<Guid> TeachingTaskIds,
    TeachingTaskSchedulingMode? SchedulingMode,
    bool? RequiresClassroom,
    IReadOnlyList<int>? AllowedDayOfWeeks,
    bool UpdateClassroomScope,
    Guid? RequiredCampusId,
    Guid? RequiredBuildingId,
    IReadOnlyList<Guid>? AllowedClassroomIds,
    bool UpdatePeriodRange,
    [Range(1, 30)] int? EarliestPeriod,
    [Range(1, 30)] int? LatestPeriod,
    bool UpdateExperimentClassroomScope = false,
    TeachingVenueNature? AllowedExperimentVenueNatures = null,
    IReadOnlyList<Guid>? AllowedExperimentClassroomIds = null,
    Guid? ExperimentRequiredCampusId = null,
    Guid? ExperimentRequiredBuildingId = null);
