using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Contracts;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Persistence;
using Jiaowu.Api.Infrastructure.Timetables;
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]
[Route("api/course-adjustments")]
public sealed class CourseAdjustmentsController(
    AppDbContext db,
    ICurrentUserDataScope currentUserDataScope) : ControllerBase
{
    private const string Applicants =
        SystemRoles.SuperAdmin + "," +
        SystemRoles.AcademicAdmin + "," +
        SystemRoles.CollegeAdmin + "," +
        SystemRoles.Teacher;

    private const string Reviewers =
        SystemRoles.SuperAdmin + "," +
        SystemRoles.AcademicAdmin + "," +
        SystemRoles.CollegeAdmin;

    [HttpGet("task-options")]
    [Authorize(Roles = Applicants)]
    public async Task<ActionResult> GetTaskOptions(
        Guid? academicTermId,
        CancellationToken cancellationToken)
    {
        var source = AccessibleTeachingTasks().AsNoTracking()
            .Where(x => x.Status == TeachingTaskStatus.Published);
        if (academicTermId.HasValue)
            source = source.Where(x => x.AcademicTermId == academicTermId);

        return Ok(await source
            .OrderByDescending(x => x.AcademicTerm!.StartDate)
            .ThenBy(x => x.Course!.Code)
            .ThenBy(x => x.TaskNumber)
            .Select(x => new
            {
                x.Id,
                x.TaskNumber,
                x.Name,
                x.AcademicTermId,
                TermName = x.AcademicTerm!.Name,
                CourseCode = x.Course!.Code,
                CourseName = x.Course.Name,
                ClassNames = x.Classes
                    .OrderBy(item => item.AdministrativeClass!.Code)
                    .Select(item => item.AdministrativeClass!.Name)
            })
            .ToListAsync(cancellationToken));
    }

    [HttpGet("tasks/{teachingTaskId:guid}/session-options")]
    [Authorize(Roles = Applicants)]
    public async Task<ActionResult> GetSessionOptions(
        Guid teachingTaskId,
        CancellationToken cancellationToken)
    {
        var task = await AccessibleTeachingTasks().AsNoTracking()
            .Where(x =>
                x.Id == teachingTaskId &&
                x.Status == TeachingTaskStatus.Published)
            .Select(x => new
            {
                x.Id,
                x.TaskNumber,
                CourseName = x.Course!.Name,
                x.AcademicTermId,
                TermStartDate = x.AcademicTerm!.StartDate,
                TermEndDate = x.AcademicTerm.EndDate
            })
            .FirstOrDefaultAsync(cancellationToken);
        if (task is null) return NotFound();

        var entries = await db.ScheduleEntries.AsNoTracking()
            .Where(x =>
                x.TeachingTaskId == teachingTaskId &&
                x.SchedulePlan!.Status == SchedulePlanStatus.Published)
            .OrderBy(x => x.DayOfWeek)
            .ThenBy(x => x.StartPeriod)
            .Select(x => new
            {
                x.Id,
                x.DayOfWeek,
                x.StartPeriod,
                x.PeriodCount,
                x.StartWeek,
                x.EndWeek,
                x.WeekPattern,
                x.ClassroomId,
                ClassroomName = x.Classroom != null ? x.Classroom.Name : null,
                BuildingName = x.Classroom != null ? x.Classroom.Building!.Name : null
            })
            .ToListAsync(cancellationToken);

        var occupied = await db.CourseAdjustments.AsNoTracking()
            .Where(x =>
                x.TeachingTaskId == teachingTaskId &&
                x.SourceScheduleEntryId != null &&
                x.SourceWeek != null &&
                (x.Status == CourseAdjustmentStatus.Submitted ||
                 x.Status == CourseAdjustmentStatus.Approved))
            .Select(x => new { x.SourceScheduleEntryId, x.SourceWeek })
            .ToListAsync(cancellationToken);
        var occupiedOccurrences = occupied
            .Select(x => (x.SourceScheduleEntryId!.Value, x.SourceWeek!.Value))
            .ToHashSet();

        var sessions = entries
            .SelectMany(entry => Enumerable.Range(
                    entry.StartWeek,
                    entry.EndWeek - entry.StartWeek + 1)
                .Where(week => IncludesWeek(entry.WeekPattern, week))
                .Select(week => new
                {
                    ScheduleEntryId = entry.Id,
                    Week = week,
                    Date = ResolveDate(task.TermStartDate, week, entry.DayOfWeek),
                    entry.DayOfWeek,
                    entry.StartPeriod,
                    entry.PeriodCount,
                    entry.ClassroomId,
                    entry.ClassroomName,
                    entry.BuildingName,
                    HasExistingAdjustment = occupiedOccurrences.Contains((entry.Id, week))
                }))
            .Where(x => x.Date >= task.TermStartDate && x.Date <= task.TermEndDate)
            .OrderBy(x => x.Date)
            .ThenBy(x => x.StartPeriod)
            .ToList();

        var classrooms = await db.Classrooms.AsNoTracking()
            .Where(x => x.IsEnabled)
            .OrderBy(x => x.Building!.Campus!.SortOrder)
            .ThenBy(x => x.Building!.SortOrder)
            .ThenBy(x => x.SortOrder)
            .Select(x => new
            {
                x.Id,
                x.Name,
                BuildingName = x.Building!.Name,
                CampusName = x.Building.Campus!.Name,
                x.Capacity
            })
            .ToListAsync(cancellationToken);
        var periods = await db.ScheduleTimeSlots.AsNoTracking()
            .Where(x => x.AcademicTermId == task.AcademicTermId && x.IsEnabled)
            .OrderBy(x => x.PeriodNumber)
            .Select(x => new
            {
                x.PeriodNumber,
                x.Name,
                StartsAt = x.StartsAt.ToString("HH:mm"),
                EndsAt = x.EndsAt.ToString("HH:mm")
            })
            .ToListAsync(cancellationToken);

        return Ok(new { Task = task, Sessions = sessions, Classrooms = classrooms, Periods = periods });
    }

    // ═══════════════ My adjustments ═══════════════

    [HttpGet("mine")]
    [Authorize(Roles = Applicants)]
    public async Task<ActionResult> GetMine(
        Guid? academicTermId,
        CourseAdjustmentStatus? status,
        int page = 1,
        int pageSize = 20,
        CancellationToken cancellationToken = default)
    {
        if (page < 1 || pageSize is < 1 or > 100)
            return ValidationProblem("页码必须大于 0，且每页条数应在 1 至 100 之间。");

        var userId = currentUserDataScope.Current.UserId;
        var source = db.CourseAdjustments.AsNoTracking()
            .Where(x => x.ApplicantUserId == userId);
        if (academicTermId.HasValue)
            source = source.Where(x =>
                x.TeachingTask!.AcademicTermId == academicTermId);
        if (status.HasValue)
            source = source.Where(x => x.Status == status);

        var total = await source.CountAsync(cancellationToken);
        var items = await source
            .OrderByDescending(x => x.CreatedAt)
            .ThenByDescending(x => x.Id)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(AdjustmentProjection())
            .ToListAsync(cancellationToken);
        return Ok(new PagedResult<object>(items, total, page, pageSize));
    }

    // ═══════════════ Pending reviews ═══════════════

    [HttpGet("pending-reviews")]
    [Authorize(Roles = Reviewers)]
    public async Task<ActionResult> GetPendingReviews(
        Guid? academicTermId,
        int page = 1,
        int pageSize = 20,
        CancellationToken cancellationToken = default)
    {
        if (page < 1 || pageSize is < 1 or > 100)
            return ValidationProblem("页码必须大于 0，且每页条数应在 1 至 100 之间。");

        var scope = currentUserDataScope.Current;
        var source = db.CourseAdjustments.AsNoTracking()
            .Where(x => x.Status == CourseAdjustmentStatus.Submitted);
        if (scope.Scope == DataScope.College)
            source = source.Where(x =>
                x.TeachingTask!.Course!.CollegeId == scope.RestrictedCollegeId);
        if (academicTermId.HasValue)
            source = source.Where(x =>
                x.TeachingTask!.AcademicTermId == academicTermId);

        var total = await source.CountAsync(cancellationToken);
        var items = await source
            .OrderByDescending(x => x.SubmittedAt)
            .ThenByDescending(x => x.Id)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(AdjustmentProjection())
            .ToListAsync(cancellationToken);
        return Ok(new PagedResult<object>(items, total, page, pageSize));
    }

    // ═══════════════ Detail ═══════════════

    [HttpGet("{id:guid}")]
    [Authorize(Roles = Applicants)]
    public async Task<ActionResult> GetDetail(Guid id, CancellationToken cancellationToken)
    {
        var adj = await db.CourseAdjustments.AsNoTracking()
            .Where(x => x.Id == id)
            .Select(AdjustmentProjection())
            .FirstOrDefaultAsync(cancellationToken);
        return adj is null ? NotFound() : Ok(adj);
    }

    // ═══════════════ Create ═══════════════

    [HttpPost]
    [Authorize(Roles = Applicants)]
    public async Task<ActionResult> Create(
        CourseAdjustmentRequest request,
        CancellationToken cancellationToken)
    {
        var userId = currentUserDataScope.Current.UserId;
        var validation = await ValidateRequestAsync(request, cancellationToken);
        if (validation is not null) return validation;

        var source = RequiresSourceOccurrence(request.Type)
            ? await LoadSourceOccurrenceAsync(
                request.TeachingTaskId,
                request.SourceScheduleEntryId,
                request.SourceWeek,
                cancellationToken)
            : null;
        var targetDayOfWeek = request.TargetDate.HasValue
            ? ToIsoDayOfWeek(request.TargetDate.Value.DayOfWeek)
            : request.DayOfWeek;

        var adj = new CourseAdjustment
        {
            TeachingTaskId = request.TeachingTaskId,
            Type = request.Type,
            ApplicantUserId = userId,
            Reason = request.Reason.Trim(),
            SourceScheduleEntryId = source?.Entry.Id,
            SourceWeek = source?.Week,
            SourceDate = source?.Date,
            SourceStartPeriod = source?.Entry.StartPeriod,
            SourcePeriodCount = source?.Entry.PeriodCount,
            SourceClassroomId = source?.Entry.ClassroomId,
            TargetDate = request.TargetDate,
            DayOfWeek = targetDayOfWeek,
            StartPeriod = request.StartPeriod,
            PeriodCount = source?.Entry.PeriodCount ?? request.PeriodCount,
            ClassroomId = request.ClassroomId,
            SubstituteTeacherId = request.SubstituteTeacherId,
            CancelWeek = request.Type == CourseAdjustmentType.Cancel
                ? source?.Week
                : request.CancelWeek,
            CancelDate = request.Type == CourseAdjustmentType.Cancel
                ? source?.Date
                : request.CancelDate
        };

        if (request.Submit)
        {
            adj.Status = CourseAdjustmentStatus.Submitted;
            adj.SubmittedAt = DateTime.UtcNow;
        }

        db.CourseAdjustments.Add(adj);
        await db.SaveChangesAsync(cancellationToken);
        await new PublishedTimetableProjectionService(db)
            .RebuildPublishedPlansForTaskAsync(adj.TeachingTaskId, cancellationToken);

        if (request.Submit)
        {
            var taskInfo = await db.TeachingTasks
                .Where(x => x.Id == request.TeachingTaskId)
                .Select(x => new { x.Course!.Name, x.Course.CollegeId })
                .FirstOrDefaultAsync(cancellationToken);
            if (taskInfo is not null)
            {
                var tl = TypeLabel(request.Type);
                await NotificationService.SendToRoleAsync(db,
                    SystemRoles.CollegeAdmin,
                    $"新的{tl}申请",
                    $"《{taskInfo.Name}》提交了{tl}申请，请及时审核。",
                    taskInfo.CollegeId, "/course-adjustments", cancellationToken,
                    NotificationCategory.Schedule);
            }
        }

        return Created(string.Empty, new { adj.Id });
    }

    // ═══════════════ Submit ═══════════════

    [HttpPost("{id:guid}/submit")]
    [Authorize(Roles = Applicants)]
    public async Task<ActionResult> Submit(Guid id, CancellationToken cancellationToken)
    {
        var userId = currentUserDataScope.Current.UserId;
        var adj = await db.CourseAdjustments
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.Course)
            .FirstOrDefaultAsync(x =>
                x.Id == id && x.ApplicantUserId == userId, cancellationToken);
        if (adj is null) return NotFound();
        if (adj.Status != CourseAdjustmentStatus.Draft)
            return ConflictProblem("只有草稿可以提交。");

        var validation = await ValidateRequestAsync(
            new CourseAdjustmentRequest(
                adj.TeachingTaskId,
                adj.Type,
                adj.Reason,
                true,
                adj.TargetDate,
                adj.DayOfWeek,
                adj.StartPeriod,
                adj.PeriodCount,
                adj.ClassroomId,
                adj.SubstituteTeacherId,
                adj.CancelWeek,
                adj.CancelDate,
                adj.SourceScheduleEntryId,
                adj.SourceWeek),
            cancellationToken);
        if (validation is not null) return validation;

        adj.Status = CourseAdjustmentStatus.Submitted;
        adj.SubmittedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(cancellationToken);

        var courseName = adj.TeachingTask!.Course!.Name;
        await NotificationService.SendToRoleAsync(db,
            SystemRoles.CollegeAdmin,
            $"新的{TypeLabel(adj.Type)}申请",
            $"《{courseName}》提交了{TypeLabel(adj.Type)}申请，请及时审核。",
            adj.TeachingTask.Course.CollegeId,
            "/course-adjustments", cancellationToken,
            NotificationCategory.Schedule);
        await NotificationService.SendToRoleAsync(db,
            SystemRoles.AcademicAdmin,
            $"新的{TypeLabel(adj.Type)}申请",
            $"《{courseName}》提交了{TypeLabel(adj.Type)}申请。",
            null, "/course-adjustments", cancellationToken,
            NotificationCategory.Schedule);
        return NoContent();
    }

    // ═══════════════ Approve ═══════════════

    [HttpPost("{id:guid}/approve")]
    [Authorize(Roles = Reviewers)]
    public async Task<ActionResult> Approve(Guid id, CancellationToken cancellationToken)
    {
        var adj = await db.CourseAdjustments
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.Course)
            .ThenInclude(x => x!.College)
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.Teachers)
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.Classes)
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.AcademicTerm)
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
        if (adj is null) return NotFound();

        var scope = currentUserDataScope.Current;
        if (scope.Scope == DataScope.College &&
            adj.TeachingTask!.Course!.CollegeId != scope.RestrictedCollegeId)
            return ConflictProblem("只能审核本学院课程的调停课申请。");
        if (adj.Status != CourseAdjustmentStatus.Submitted)
            return ConflictProblem("只有待审核的申请可以审批。");

        var scheduleProblem = await ValidateApprovalScheduleAsync(
            adj,
            cancellationToken);
        if (scheduleProblem is not null)
            return ConflictProblem(scheduleProblem);

        adj.Status = CourseAdjustmentStatus.Approved;
        adj.ReviewedAt = DateTime.UtcNow;
        adj.ReviewedByUserId = currentUserDataScope.Current.UserId;

        // Apply schedule changes
        await ApplyScheduleChangesAsync(adj, cancellationToken);

        await db.SaveChangesAsync(cancellationToken);

        // Notify applicant
        await NotificationService.SendAsync(db, adj.ApplicantUserId,
            "调停课申请已通过",
            $"您的{TypeLabel(adj.Type)}申请（{adj.TeachingTask!.Course!.Name}）已通过审核。",
            "/course-adjustments", cancellationToken,
            NotificationCategory.Schedule);

        // Notify affected students
        var studentUserIds = await TeachingTaskRosterQuery
            .ForTask(db, adj.TeachingTaskId)
            .Select(x => x.UserId)
            .Where(uid => uid != null)
            .Select(uid => uid!.Value)
            .Distinct()
            .ToListAsync(cancellationToken);
        if (studentUserIds.Count > 0)
        {
            await NotificationService.SendToUserIdsAsync(db, studentUserIds,
                "课程变动通知",
                $"《{adj.TeachingTask.Course.Name}》有{TypeLabel(adj.Type)}变动，请查看课表。",
                "/my-timetable", cancellationToken,
                NotificationCategory.Schedule);
        }

        return NoContent();
    }

    private async Task ApplyScheduleChangesAsync(
        CourseAdjustment adj, CancellationToken ct)
    {
        switch (adj.Type)
        {
            case CourseAdjustmentType.Reschedule:
                var rescheduleSource = await LoadSourceEntryAsync(adj, ct);
                ExcludeWeek(rescheduleSource, adj.SourceWeek!.Value);
                db.ScheduleEntries.Add(CreateTargetEntry(adj, rescheduleSource.SchedulePlanId));
                break;

            case CourseAdjustmentType.Cancel:
                var cancelSource = await LoadSourceEntryAsync(adj, ct);
                ExcludeWeek(cancelSource, adj.SourceWeek!.Value);
                break;

            case CourseAdjustmentType.Makeup:
                var makeupSource = await LoadSourceEntryAsync(adj, ct);
                db.ScheduleEntries.Add(CreateTargetEntry(adj, makeupSource.SchedulePlanId));
                break;

            case CourseAdjustmentType.Substitute:
                // Substitute: add substitute teacher to the teaching task
                if (adj.SubstituteTeacherId.HasValue)
                {
                    var alreadyExists = await db.TeachingTaskTeachers
                        .AnyAsync(x =>
                            x.TeachingTaskId == adj.TeachingTaskId &&
                            x.TeacherId == adj.SubstituteTeacherId.Value, ct);
                    if (!alreadyExists)
                    {
                        db.TeachingTaskTeachers.Add(new TeachingTaskTeacher
                        {
                            TeachingTaskId = adj.TeachingTaskId,
                            TeacherId = adj.SubstituteTeacherId.Value,
                            IsPrimary = false
                        });
                    }
                }
                break;
        }
    }

    private static string TypeLabel(CourseAdjustmentType type) => type switch
    {
        CourseAdjustmentType.Reschedule => "调课",
        CourseAdjustmentType.Cancel => "停课",
        CourseAdjustmentType.Makeup => "补课",
        CourseAdjustmentType.Substitute => "代课",
        _ => "调停课"
    };

    // ═══════════════ Reject ═══════════════

    [HttpPost("{id:guid}/reject")]
    [Authorize(Roles = Reviewers)]
    public async Task<ActionResult> Reject(
        Guid id,
        RejectionRequest request,
        CancellationToken cancellationToken)
    {
        var adj = await db.CourseAdjustments
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.Course)
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
        if (adj is null) return NotFound();

        var scope = currentUserDataScope.Current;
        if (scope.Scope == DataScope.College &&
            adj.TeachingTask!.Course!.CollegeId != scope.RestrictedCollegeId)
            return ConflictProblem("只能审核本学院课程的调停课申请。");
        if (adj.Status != CourseAdjustmentStatus.Submitted)
            return ConflictProblem("只有待审核的申请可以退回。");

        adj.Status = CourseAdjustmentStatus.Rejected;
        adj.ReviewComment = request.Comment?.Trim();
        adj.ReviewedAt = DateTime.UtcNow;
        adj.ReviewedByUserId = currentUserDataScope.Current.UserId;
        await db.SaveChangesAsync(cancellationToken);

        var msg = adj.ReviewComment is not null
            ? $"您的{TypeLabel(adj.Type)}申请已退回。审核意见：{adj.ReviewComment}"
            : $"您的{TypeLabel(adj.Type)}申请已退回。";
        await NotificationService.SendAsync(db, adj.ApplicantUserId,
            $"{TypeLabel(adj.Type)}申请已退回", msg,
            "/course-adjustments", cancellationToken,
            NotificationCategory.Schedule);
        return NoContent();
    }

    // ═══════════════ Helpers ═══════════════

    private async Task<ActionResult?> ValidateRequestAsync(
        CourseAdjustmentRequest request,
        CancellationToken cancellationToken)
    {
        var task = await AccessibleTeachingTasks().AsNoTracking()
            .Include(x => x.Course)
            .FirstOrDefaultAsync(x =>
                x.Id == request.TeachingTaskId &&
                x.Status == TeachingTaskStatus.Published,
                cancellationToken);
        if (task is null) return ValidationProblem("教学班不存在或未发布。");

        switch (request.Type)
        {
            case CourseAdjustmentType.Reschedule:
            case CourseAdjustmentType.Makeup:
                if (!request.TargetDate.HasValue)
                    return ValidationProblem(
                        request.Type == CourseAdjustmentType.Reschedule
                            ? "请选择调课后的日期。"
                            : "请选择补课日期。");
                if (!request.StartPeriod.HasValue || request.StartPeriod < 1)
                    return ValidationProblem("请选择起始节次。");
                break;
            case CourseAdjustmentType.Substitute:
                if (!request.SubstituteTeacherId.HasValue)
                    return ValidationProblem("请选择代课教师。");
                if (!await db.Teachers.AnyAsync(x =>
                        x.Id == request.SubstituteTeacherId &&
                        x.Status == TeacherStatus.Active, cancellationToken))
                    return ValidationProblem("代课教师不存在或已离职。");
                break;
            case CourseAdjustmentType.Cancel:
                break;
        }

        if (RequiresSourceOccurrence(request.Type))
        {
            var source = await LoadSourceOccurrenceAsync(
                request.TeachingTaskId,
                request.SourceScheduleEntryId,
                request.SourceWeek,
                cancellationToken);
            if (source is null)
                return ValidationProblem("请选择当前已发布课表中的具体原课次。");

            var duplicate = await db.CourseAdjustments.AsNoTracking().AnyAsync(x =>
                x.SourceScheduleEntryId == source.Entry.Id &&
                x.SourceWeek == source.Week &&
                (x.Status == CourseAdjustmentStatus.Submitted ||
                 x.Status == CourseAdjustmentStatus.Approved),
                cancellationToken);
            if (duplicate)
                return ConflictProblem("该课次已有待审核或已通过的调停课记录。");

            if (request.TargetDate.HasValue &&
                (request.TargetDate < source.Term.StartDate ||
                 request.TargetDate > source.Term.EndDate))
                return ValidationProblem("目标日期必须在本学期起止日期内。");
        }

        if (request.ClassroomId.HasValue &&
            !await db.Classrooms.AsNoTracking().AnyAsync(x =>
                x.Id == request.ClassroomId && x.IsEnabled,
                cancellationToken))
            return ValidationProblem("目标教室不存在或已停用。");

        return null;
    }

    private async Task<string?> ValidateApprovalScheduleAsync(
        CourseAdjustment adjustment,
        CancellationToken cancellationToken)
    {
        if (!RequiresSourceOccurrence(adjustment.Type)) return null;

        var source = await LoadSourceOccurrenceAsync(
            adjustment.TeachingTaskId,
            adjustment.SourceScheduleEntryId,
            adjustment.SourceWeek,
            cancellationToken);
        if (source is null)
            return "原课次已不在当前发布课表中，请退回后重新选择。";
        if (adjustment.Type == CourseAdjustmentType.Cancel) return null;
        if (!adjustment.TargetDate.HasValue || !adjustment.StartPeriod.HasValue)
            return "目标上课日期或节次不完整。";

        var targetWeek = ResolveWeek(source.Term.StartDate, adjustment.TargetDate.Value);
        var targetDay = ToIsoDayOfWeek(adjustment.TargetDate.Value.DayOfWeek);
        var target = new ScheduleEntry
        {
            TeachingTaskId = adjustment.TeachingTaskId,
            TeachingTask = adjustment.TeachingTask,
            ClassroomId = adjustment.ClassroomId ?? adjustment.SourceClassroomId,
            DayOfWeek = targetDay,
            StartPeriod = adjustment.StartPeriod.Value,
            PeriodCount = adjustment.SourcePeriodCount ?? source.Entry.PeriodCount,
            StartWeek = targetWeek,
            EndWeek = targetWeek,
            WeekPattern = WeekPattern.All
        };
        var possibleConflicts = await db.ScheduleEntries.AsNoTracking()
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.Teachers)
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.Classes)
            .Where(x =>
                x.SchedulePlan!.Status == SchedulePlanStatus.Published &&
                x.Id != adjustment.SourceScheduleEntryId &&
                x.DayOfWeek == targetDay &&
                x.StartWeek <= targetWeek &&
                x.EndWeek >= targetWeek &&
                x.StartPeriod < target.StartPeriod + target.PeriodCount &&
                target.StartPeriod < x.StartPeriod + x.PeriodCount)
            .ToListAsync(cancellationToken);
        foreach (var existing in possibleConflicts.Where(x =>
                     IncludesWeek(x.WeekPattern, targetWeek)))
        {
            var reason = ScheduleConflictDetector.ConflictReason(target, existing);
            if (reason is not null)
                return $"目标时间存在{reason}冲突：{existing.TeachingTask!.Name}。";
        }

        return null;
    }

    private async Task<SourceOccurrence?> LoadSourceOccurrenceAsync(
        Guid teachingTaskId,
        Guid? scheduleEntryId,
        int? week,
        CancellationToken cancellationToken)
    {
        if (!scheduleEntryId.HasValue || !week.HasValue) return null;
        var entry = await db.ScheduleEntries.AsNoTracking()
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.AcademicTerm)
            .FirstOrDefaultAsync(x =>
                x.Id == scheduleEntryId.Value &&
                x.TeachingTaskId == teachingTaskId &&
                x.SchedulePlan!.Status == SchedulePlanStatus.Published,
                cancellationToken);
        if (entry?.TeachingTask?.AcademicTerm is null ||
            week < entry.StartWeek || week > entry.EndWeek ||
            !IncludesWeek(entry.WeekPattern, week.Value))
            return null;
        var date = ResolveDate(
            entry.TeachingTask.AcademicTerm.StartDate,
            week.Value,
            entry.DayOfWeek);
        if (date < entry.TeachingTask.AcademicTerm.StartDate ||
            date > entry.TeachingTask.AcademicTerm.EndDate)
            return null;
        return new SourceOccurrence(
            entry,
            entry.TeachingTask.AcademicTerm,
            week.Value,
            date);
    }

    private Task<ScheduleEntry> LoadSourceEntryAsync(
        CourseAdjustment adjustment,
        CancellationToken cancellationToken) =>
        db.ScheduleEntries.SingleAsync(x =>
            x.Id == adjustment.SourceScheduleEntryId &&
            x.TeachingTaskId == adjustment.TeachingTaskId &&
            x.SchedulePlan!.Status == SchedulePlanStatus.Published,
            cancellationToken);

    private void ExcludeWeek(ScheduleEntry entry, int week)
    {
        db.ScheduleEntries.Remove(entry);
        if (entry.StartWeek <= week - 1 &&
            HasIncludedWeek(entry.WeekPattern, entry.StartWeek, week - 1))
            db.ScheduleEntries.Add(CloneEntry(entry, entry.StartWeek, week - 1));
        if (week + 1 <= entry.EndWeek &&
            HasIncludedWeek(entry.WeekPattern, week + 1, entry.EndWeek))
            db.ScheduleEntries.Add(CloneEntry(entry, week + 1, entry.EndWeek));
    }

    private ScheduleEntry CreateTargetEntry(
        CourseAdjustment adjustment,
        Guid schedulePlanId)
    {
        var targetWeek = ResolveWeek(
            adjustment.TeachingTask!.AcademicTerm!.StartDate,
            adjustment.TargetDate!.Value);
        return new ScheduleEntry
        {
            SchedulePlanId = schedulePlanId,
            TeachingTaskId = adjustment.TeachingTaskId,
            ClassroomId = adjustment.ClassroomId ?? adjustment.SourceClassroomId,
            DayOfWeek = ToIsoDayOfWeek(adjustment.TargetDate.Value.DayOfWeek),
            StartPeriod = adjustment.StartPeriod!.Value,
            PeriodCount = adjustment.SourcePeriodCount!.Value,
            StartWeek = targetWeek,
            EndWeek = targetWeek,
            WeekPattern = WeekPattern.All,
            Notes = adjustment.Type == CourseAdjustmentType.Reschedule
                ? $"调课（原第 {adjustment.SourceWeek} 周）"
                : $"补课（对应原第 {adjustment.SourceWeek} 周课次）"
        };
    }

    private static ScheduleEntry CloneEntry(
        ScheduleEntry source,
        int startWeek,
        int endWeek) => new()
    {
        SchedulePlanId = source.SchedulePlanId,
        TeachingTaskId = source.TeachingTaskId,
        ClassroomId = source.ClassroomId,
        DayOfWeek = source.DayOfWeek,
        StartPeriod = source.StartPeriod,
        PeriodCount = source.PeriodCount,
        StartWeek = startWeek,
        EndWeek = endWeek,
        WeekPattern = source.WeekPattern,
        Notes = source.Notes
    };

    private static bool RequiresSourceOccurrence(CourseAdjustmentType type) =>
        type is CourseAdjustmentType.Reschedule or
            CourseAdjustmentType.Cancel or
            CourseAdjustmentType.Makeup;

    private static bool IncludesWeek(WeekPattern pattern, int week) =>
        pattern == WeekPattern.All ||
        pattern == WeekPattern.Odd && week % 2 == 1 ||
        pattern == WeekPattern.Even && week % 2 == 0;

    private static bool HasIncludedWeek(WeekPattern pattern, int startWeek, int endWeek) =>
        Enumerable.Range(startWeek, endWeek - startWeek + 1)
            .Any(week => IncludesWeek(pattern, week));

    private static DateOnly ResolveDate(DateOnly termStartDate, int week, int dayOfWeek)
    {
        var startDay = (int)termStartDate.DayOfWeek;
        var daysSinceMonday = (startDay + 6) % 7;
        var firstWeekMonday = termStartDate.AddDays(-daysSinceMonday);
        return firstWeekMonday.AddDays((week - 1) * 7 + dayOfWeek - 1);
    }

    private static int ResolveWeek(DateOnly termStartDate, DateOnly date)
    {
        var startDay = (int)termStartDate.DayOfWeek;
        var daysSinceMonday = (startDay + 6) % 7;
        var firstWeekMonday = termStartDate.AddDays(-daysSinceMonday);
        return (date.DayNumber - firstWeekMonday.DayNumber) / 7 + 1;
    }

    private static int ToIsoDayOfWeek(DayOfWeek dayOfWeek) =>
        dayOfWeek == System.DayOfWeek.Sunday ? 7 : (int)dayOfWeek;

    private IQueryable<TeachingTask> AccessibleTeachingTasks()
    {
        var source = db.TeachingTasks.AsQueryable();
        var scope = currentUserDataScope.Current;
        if (scope.Scope == DataScope.All) return source;
        if (scope.Scope == DataScope.College)
            return source.Where(x =>
                x.Course!.CollegeId == scope.RestrictedCollegeId);
        if (scope.IsInRole(SystemRoles.Teacher))
            return source.Where(x =>
                x.Teachers.Any(item => item.Teacher!.UserId == scope.UserId));
        return source.Where(_ => false);
    }

    private System.Linq.Expressions.Expression<
        Func<CourseAdjustment, object>> AdjustmentProjection() => x => new
    {
        x.Id,
        x.TeachingTaskId,
        x.TeachingTask!.TaskNumber,
        TaskName = x.TeachingTask.Name,
        CourseCode = x.TeachingTask.Course!.Code,
        CourseName = x.TeachingTask.Course.Name,
        CollegeName = x.TeachingTask.Course.College!.Name,
        TeacherNames = x.TeachingTask.Teachers
            .OrderByDescending(t => t.IsPrimary)
            .Select(t => t.Teacher!.Name),
        x.Type,
        x.Status,
        x.Reason,
        x.ReviewComment,
        x.SourceScheduleEntryId,
        x.SourceWeek,
        x.SourceDate,
        x.SourceStartPeriod,
        x.SourcePeriodCount,
        SourceClassroomName = x.SourceClassroomId == null
            ? null
            : db.Classrooms
                .Where(room => room.Id == x.SourceClassroomId)
                .Select(room => room.Building!.Name + " " + room.Name)
                .FirstOrDefault(),
        x.TargetDate,
        x.DayOfWeek,
        x.StartPeriod,
        x.PeriodCount,
        ClassroomName = x.Classroom != null ? x.Classroom.Name : null,
        BuildingName = x.Classroom != null ? x.Classroom.Building!.Name : null,
        SubstituteTeacherName = x.SubstituteTeacher != null ? x.SubstituteTeacher.Name : null,
        x.CancelWeek,
        x.CancelDate,
        x.ApplicantUserId,
        x.SubmittedAt,
        x.ReviewedAt,
        x.CreatedAt
    };

    private sealed record SourceOccurrence(
        ScheduleEntry Entry,
        AcademicTerm Term,
        int Week,
        DateOnly Date);

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

// ═══════════════ Request records ═══════════════

public sealed record CourseAdjustmentRequest(
    Guid TeachingTaskId,
    CourseAdjustmentType Type,
    [Required, MaxLength(500)] string Reason,
    bool Submit,
    DateOnly? TargetDate,
    [Range(1, 7)] int? DayOfWeek,
    [Range(1, 30)] int? StartPeriod,
    [Range(1, 6)] int? PeriodCount,
    Guid? ClassroomId,
    Guid? SubstituteTeacherId,
    int? CancelWeek,
    DateOnly? CancelDate,
    Guid? SourceScheduleEntryId = null,
    int? SourceWeek = null);

public sealed record RejectionRequest(
    [MaxLength(500)] string? Comment);
