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

namespace Jiaowu.Api.Infrastructure.Timetables;

public sealed class TimetableDataService(AppDbContext db)
{
    public async Task<TimetableData?> BuildAsync(
        TimetableResourceType resourceType,
        Guid resourceId,
        Guid? academicTermId,
        Guid? schedulePlanId,
        bool allowUnpublishedPlan,
        Guid? studentId,
        TimetableStudentDto? student,
        CancellationToken cancellationToken)
    {
        var subject = await LoadSubjectAsync(resourceType, resourceId, cancellationToken);
        if (subject is null) return null;

        var term = await ResolveTermAsync(academicTermId, cancellationToken);
        if (term is null) return null;
        var plan = await ResolvePlanAsync(
            term.Id,
            schedulePlanId,
            allowUnpublishedPlan,
            cancellationToken);
        var slots = await db.ScheduleTimeSlots.AsNoTracking()
            .TagWith("Timetable.LoadTimeSlots")
            .Where(x => x.AcademicTermId == term.Id && x.IsEnabled)
            .OrderBy(x => x.PeriodNumber)
            .Select(x => new TimetableSlotDto(
                x.PeriodNumber,
                x.Name,
                x.StartsAt,
                x.EndsAt))
            .ToListAsync(cancellationToken);

        var entries = new List<TimetableEntryDto>();
        if (plan is not null)
        {
            var source = db.ScheduleEntries.AsNoTracking()
                .Where(x => x.SchedulePlanId == plan.Id);
            switch (resourceType)
            {
                case TimetableResourceType.Class:
                    if (studentId.HasValue)
                    {
                        var selectedTaskIds = db.CourseEnrollments.AsNoTracking()
                            .Where(x =>
                                x.StudentId == studentId.Value &&
                                x.Status == CourseEnrollmentStatus.Enrolled &&
                                x.CourseSelectionOffering!.CourseSelectionRound!
                                    .AcademicTermId == term.Id)
                            .Select(x => x.CourseSelectionOffering!.TeachingTaskId);
                        source = source.Where(x =>
                            x.TeachingTask!.Classes.Any(item =>
                                item.AdministrativeClassId == resourceId) ||
                            selectedTaskIds.Contains(x.TeachingTaskId));
                    }
                    else
                    {
                        source = source.Where(x =>
                            x.TeachingTask!.Classes.Any(item =>
                                item.AdministrativeClassId == resourceId));
                    }
                    break;
                case TimetableResourceType.Teacher:
                    source = source.Where(x =>
                        x.TeachingTask!.Teachers.Any(item => item.TeacherId == resourceId));
                    break;
                case TimetableResourceType.Classroom:
                    source = source.Where(x => x.ClassroomId == resourceId);
                    break;
            }

            entries = await source
                .TagWith("Timetable.LoadScheduleEntries")
                .OrderBy(x => x.DayOfWeek)
                .ThenBy(x => x.StartPeriod)
                .ThenBy(x => x.TeachingTask!.Course!.Code)
                .Select(x => new TimetableEntryDto(
                    x.Id,
                    x.TeachingTaskId,
                    x.TeachingTask!.TaskNumber,
                    x.TeachingTask.Name,
                    x.TeachingTask.Course!.Code,
                    x.TeachingTask.Course.Name,
                    x.TeachingTask.Teachers
                        .OrderByDescending(item => item.IsPrimary)
                        .Select(item => item.Teacher!.Name),
                    x.TeachingTask.Classes
                        .Select(item => item.AdministrativeClass!.Name),
                    x.Classroom == null ? "不占用教室" : x.Classroom.Name,
                    x.Classroom == null ? null : x.Classroom.Building!.Name,
                    x.Classroom == null ? null : x.Classroom.Building!.Campus!.Name,
                    x.DayOfWeek,
                    x.StartPeriod,
                    x.PeriodCount,
                    x.StartWeek,
                    x.EndWeek,
                    x.WeekPattern,
                    x.Notes,
                    false,
                    null,
                    null,
                    x.UpdatedAt,
                    false,
                    null,
                    null,
                    null,
                    null,
                    null,
                    x.Kind))
                .ToListAsync(cancellationToken);
        }

        var flexibleCourses = await LoadFlexibleCoursesAsync(
            resourceType,
            resourceId,
            term.Id,
            studentId,
            cancellationToken);

        // Load exam sessions for student/teacher timetables
        var examEntries = await LoadExamEntriesAsync(
            resourceType, resourceId, term.Id, studentId, slots, cancellationToken);
        var experimentEntries = await LoadExperimentEntriesAsync(
            resourceType, term, studentId, cancellationToken);

        return new TimetableData(
            term,
            subject,
            resourceType == TimetableResourceType.Class ? subject : null,
            student,
            plan,
            slots,
            entries,
            flexibleCourses,
            examEntries)
        {
            ExperimentEntries = experimentEntries
        };
    }

    private async Task<TimetableSubjectDto?> LoadSubjectAsync(
        TimetableResourceType resourceType,
        Guid resourceId,
        CancellationToken cancellationToken)
    {
        return resourceType switch
        {
            TimetableResourceType.Class => await db.AdministrativeClasses.AsNoTracking()
                .Where(x => x.Id == resourceId && x.IsEnabled)
                .Select(x => new TimetableSubjectDto(
                    x.Id,
                    x.Code,
                    x.Name,
                    TimetableResourceType.Class,
                    x.Grade,
                    x.MajorId,
                    x.Major!.Name,
                    x.Major.CollegeId,
                    x.Major.College!.Name,
                    null,
                    null,
                    null))
                .FirstOrDefaultAsync(cancellationToken),
            TimetableResourceType.Teacher => await db.Teachers.AsNoTracking()
                .Where(x => x.Id == resourceId && x.Status == TeacherStatus.Active)
                .Select(x => new TimetableSubjectDto(
                    x.Id,
                    x.TeacherNumber,
                    x.Name,
                    TimetableResourceType.Teacher,
                    null,
                    null,
                    null,
                    x.CollegeId,
                    x.College!.Name,
                    null,
                    null,
                    x.Title))
                .FirstOrDefaultAsync(cancellationToken),
            TimetableResourceType.Classroom => await db.Classrooms.AsNoTracking()
                .Where(x => x.Id == resourceId && x.IsEnabled)
                .Select(x => new TimetableSubjectDto(
                    x.Id,
                    x.Code,
                    x.Name,
                    TimetableResourceType.Classroom,
                    null,
                    null,
                    null,
                    null,
                    null,
                    x.Building!.CampusId,
                    x.BuildingId,
                    $"{x.Building.Campus!.Name} · {x.Building.Name} · {x.Capacity} 人"))
                .FirstOrDefaultAsync(cancellationToken),
            _ => null
        };
    }

    private async Task<TimetableTermDto?> ResolveTermAsync(
        Guid? academicTermId,
        CancellationToken cancellationToken)
    {
        var query = db.AcademicTerms.AsNoTracking().Where(x => x.IsEnabled);
        if (academicTermId.HasValue)
            query = query.Where(x => x.Id == academicTermId);
        else
            query = query.OrderByDescending(x => x.IsCurrent)
                .ThenBy(x => x.IsArchived)
                .ThenByDescending(x => x.StartDate);
        return await query.Select(x => new TimetableTermDto(
                x.Id,
                x.Name,
                x.AcademicYear,
                x.Season,
                x.StartDate,
                x.EndDate,
                x.IsCurrent))
            .FirstOrDefaultAsync(cancellationToken);
    }

    private async Task<TimetablePlanDto?> ResolvePlanAsync(
        Guid academicTermId,
        Guid? schedulePlanId,
        bool allowUnpublishedPlan,
        CancellationToken cancellationToken)
    {
        var query = db.SchedulePlans.AsNoTracking()
            .Where(x => x.AcademicTermId == academicTermId);
        if (schedulePlanId.HasValue)
            query = query.Where(x => x.Id == schedulePlanId);
        if (!allowUnpublishedPlan)
            query = query.Where(x => x.Status == SchedulePlanStatus.Published);
        else
            query = query.Where(x => x.Status != SchedulePlanStatus.Archived);
        return await query
            .OrderByDescending(x => x.Status == SchedulePlanStatus.Published)
            .ThenByDescending(x => x.PublishedAt)
            .ThenByDescending(x => x.CreatedAt)
            .Select(x => new TimetablePlanDto(
                x.Id,
                x.Name,
                x.Version,
                x.Status,
                x.PublishedAt,
                x.UpdatedAt))
            .FirstOrDefaultAsync(cancellationToken);
    }

    private async Task<List<FlexibleCourseDto>> LoadFlexibleCoursesAsync(
        TimetableResourceType resourceType,
        Guid resourceId,
        Guid academicTermId,
        Guid? studentId,
        CancellationToken cancellationToken)
    {
        if (resourceType == TimetableResourceType.Classroom) return [];
        var source = db.TeachingTasks.AsNoTracking()
            .Where(x =>
                x.AcademicTermId == academicTermId &&
                x.Status == TeachingTaskStatus.Published &&
                x.SchedulingMode == TeachingTaskSchedulingMode.Flexible);
        if (resourceType == TimetableResourceType.Teacher)
        {
            source = source.Where(x =>
                x.Teachers.Any(item => item.TeacherId == resourceId));
        }
        else if (studentId.HasValue)
        {
            var selectedTaskIds = db.CourseEnrollments.AsNoTracking()
                .Where(x =>
                    x.StudentId == studentId.Value &&
                    x.Status == CourseEnrollmentStatus.Enrolled &&
                    x.CourseSelectionOffering!.CourseSelectionRound!
                        .AcademicTermId == academicTermId)
                .Select(x => x.CourseSelectionOffering!.TeachingTaskId);
            source = source.Where(x =>
                x.Classes.Any(item => item.AdministrativeClassId == resourceId) ||
                selectedTaskIds.Contains(x.Id));
        }
        else
        {
            source = source.Where(x =>
                x.Classes.Any(item => item.AdministrativeClassId == resourceId));
        }

        return await source
            .TagWith("Timetable.LoadFlexibleCourses")
            .OrderBy(x => x.Course!.Code)
            .ThenBy(x => x.TaskNumber)
            .Select(x => new FlexibleCourseDto(
                x.Id,
                x.TaskNumber,
                x.Name,
                x.Course!.Code,
                x.Course.Name,
                x.Course.Credits,
                x.Course.TotalHours,
                x.StartWeek,
                x.EndWeek,
                x.WeeklyHours,
                x.Teachers
                    .OrderByDescending(item => item.IsPrimary)
                    .Select(item => item.Teacher!.Name),
                x.Classes.Select(item => item.AdministrativeClass!.Name),
                x.Notes,
                x.UpdatedAt))
            .ToListAsync(cancellationToken);
    }

    private async Task<List<TimetableEntryDto>> LoadExamEntriesAsync(
        TimetableResourceType resourceType,
        Guid resourceId,
        Guid academicTermId,
        Guid? studentId,
        IReadOnlyList<TimetableSlotDto> slots,
        CancellationToken cancellationToken)
    {
        var slotLookup = slots.ToDictionary(x => x.PeriodNumber);
        var result = new List<TimetableEntryDto>();

        // ── Legacy sessions (ExamSessions with direct ClassroomId) ──
        var legacyQuery = db.ExamSessions.AsNoTracking()
            .Where(x => x.ExamPlan!.AcademicTermId == academicTermId &&
                        x.ExamPlan.Status == ExamPlanStatus.Published &&
                        x.ClassroomId != null);

        legacyQuery = ApplyLegacyResourceFilter(
            legacyQuery, resourceType, resourceId, studentId);

        var legacySessions = await legacyQuery
            .TagWith("Timetable.LoadLegacyExamEntries")
            .OrderBy(x => x.ExamDate)
            .ThenBy(x => x.StartPeriod)
            .Select(x => new ExamSessionProjection(
                x.Id,
                x.Id,
                x.TeachingTaskId,
                x.TeachingTask!.TaskNumber,
                x.TeachingTask.Name,
                x.TeachingTask.Course!.Code,
                x.TeachingTask.Course.Name,
                x.TeachingTask.Teachers
                    .OrderByDescending(t => t.IsPrimary)
                    .Select(t => t.Teacher!.Name).ToList(),
                x.TeachingTask.Classes
                    .Select(c => c.AdministrativeClass!.Name).ToList(),
                x.Classroom!.Name,
                x.Classroom.Building!.Name,
                x.Classroom.Building.Campus!.Name,
                x.ExamDate,
                x.StartPeriod,
                x.PeriodCount,
                x.ExamPlan!.Name,
                x.Invigilators.Select(i => i.Teacher!.Name).ToList(),
                x.Notes,
                x.UpdatedAt))
            .ToListAsync(cancellationToken);

        result.AddRange(legacySessions.Select(x =>
            MapToEntryDto(x, slotLookup)));

        // ── Mixed-room sessions (ExamRoomAssignment) ──
        // Start from room/session links so the resource predicate stays in SQL.
        // Loading every room, seat and roster for the term made a single
        // timetable request scale with the entire exam plan.
        var mixedQuery = db.ExamRoomSessions.AsNoTracking()
            .Where(link =>
                link.ExamRoom!.ExamPlan!.AcademicTermId == academicTermId &&
                link.ExamRoom.ExamPlan.Status == ExamPlanStatus.Published);
        mixedQuery = ApplyMixedResourceFilter(
            mixedQuery, resourceType, resourceId, studentId);

        var mixedSessions = await mixedQuery
            .TagWith("Timetable.LoadMixedExamEntries")
            .AsSplitQuery()
            .OrderBy(link => link.ExamRoom!.ExamDate)
            .ThenBy(link => link.ExamRoom!.StartPeriod)
            .ThenBy(link => link.ExamSession!.TeachingTask!.Course!.Code)
            .Select(link => new ExamSessionProjection(
                link.ExamSessionId,
                link.ExamRoomId,
                link.ExamSession!.TeachingTaskId,
                link.ExamSession.TeachingTask!.TaskNumber,
                link.ExamSession.TeachingTask.Name,
                link.ExamSession.TeachingTask.Course!.Code,
                link.ExamSession.TeachingTask.Course.Name,
                link.ExamSession.TeachingTask.Teachers
                    .OrderByDescending(t => t.IsPrimary)
                    .Select(t => t.Teacher!.Name).ToList(),
                link.ExamSession.TeachingTask.Classes
                    .Select(c => c.AdministrativeClass!.Name).ToList(),
                link.ExamRoom!.Classroom!.Name,
                link.ExamRoom.Classroom.Building!.Name,
                link.ExamRoom.Classroom.Building.Campus!.Name,
                link.ExamRoom.ExamDate,
                link.ExamRoom.StartPeriod,
                link.ExamRoom.PeriodCount,
                link.ExamRoom.ExamPlan!.Name,
                link.ExamRoom.Invigilators
                    .Select(i => i.Teacher!.Name).ToList(),
                null,
                link.ExamRoom.UpdatedAt))
            .ToListAsync(cancellationToken);

        if (resourceType == TimetableResourceType.Class && !studentId.HasValue)
        {
            result.AddRange(mixedSessions
                .GroupBy(x => x.ExamSessionId)
                .Select(group => MapClassExamEntryDto(group, slotLookup)));
        }
        else
        {
            result.AddRange(mixedSessions.Select(x =>
                MapToEntryDto(x, slotLookup)));
        }

        return result;
    }

    private async Task<List<TimetableEntryDto>> LoadExperimentEntriesAsync(
        TimetableResourceType resourceType,
        TimetableTermDto term,
        Guid? studentId,
        CancellationToken cancellationToken)
    {
        if (resourceType != TimetableResourceType.Class || !studentId.HasValue)
            return [];

        var rosterTaskIds = TeachingTaskRosterQuery.TaskIdsForStudent(
            db,
            studentId.Value);
        var sessions = await db.ExperimentSessions.AsNoTracking()
            .TagWith("Timetable.LoadExperimentEntries")
            .AsSplitQuery()
            .Where(x =>
                x.ExperimentProject!.TeachingTask!.AcademicTermId == term.Id &&
                (x.ExperimentProject.Status ==
                    ExperimentProjectStatus.Published ||
                 x.ExperimentProject.Status ==
                    ExperimentProjectStatus.Closed) &&
                x.Status == ExperimentSessionStatus.Scheduled)
            .Where(x =>
                (x.ExperimentProject!.ArrangementMode ==
                    ExperimentArrangementMode.Centralized &&
                 rosterTaskIds.Contains(
                     x.ExperimentProject.TeachingTaskId)) ||
                (x.ExperimentProject.ArrangementMode ==
                    ExperimentArrangementMode.SelfScheduled &&
                 x.Bookings.Any(booking =>
                     booking.StudentId == studentId.Value &&
                     booking.Status == ExperimentBookingStatus.Booked)))
            .OrderBy(x => x.SessionDate)
            .ThenBy(x => x.StartPeriod)
            .ThenBy(x => x.ExperimentProject!.Code)
            .Select(x => new ExperimentSessionProjection(
                x.Id,
                x.ExperimentProjectId,
                x.ExperimentProject!.Code,
                x.ExperimentProject.Name,
                x.ExperimentProject.ArrangementMode,
                x.ExperimentProject.TeachingTaskId,
                x.ExperimentProject.TeachingTask!.TaskNumber,
                x.ExperimentProject.TeachingTask.Name,
                x.ExperimentProject.TeachingTask.Course!.Code,
                x.ExperimentProject.TeachingTask.Course.Name,
                x.ExperimentProject.TeachingTask.Teachers
                    .OrderByDescending(item => item.IsPrimary)
                    .Select(item => item.Teacher!.Name)
                    .ToList(),
                x.ExperimentProject.TeachingTask.Classes
                    .Select(item => item.AdministrativeClass!.Name)
                    .ToList(),
                x.Classroom!.Name,
                x.Classroom.Building!.Name,
                x.Classroom.Building.Campus!.Name,
                x.SessionDate,
                x.StartPeriod,
                x.PeriodCount,
                x.Notes,
                x.UpdatedAt))
            .ToListAsync(cancellationToken);

        var termMonday = StartOfWeek(term.StartDate);
        return sessions.Select(x =>
        {
            var week = (x.SessionDate.DayNumber - termMonday.DayNumber) / 7 + 1;
            var dayOfWeek = x.SessionDate.DayOfWeek == DayOfWeek.Sunday
                ? 7
                : (int)x.SessionDate.DayOfWeek;
            return new TimetableEntryDto(
                x.Id,
                x.TeachingTaskId,
                x.TaskNumber,
                x.TaskName,
                x.CourseCode,
                x.CourseName,
                x.TeacherNames,
                x.ClassNames,
                x.ClassroomName,
                x.BuildingName,
                x.CampusName,
                dayOfWeek,
                x.StartPeriod,
                x.PeriodCount,
                week,
                week,
                WeekPattern.All,
                x.Notes,
                false,
                null,
                null,
                x.UpdatedAt,
                true,
                x.ExperimentProjectId,
                x.ProjectCode,
                x.ProjectName,
                x.SessionDate,
                x.ArrangementMode);
        }).ToList();
    }

    private static DateOnly StartOfWeek(DateOnly date)
    {
        var dayOfWeek = date.DayOfWeek == DayOfWeek.Sunday
            ? 7
            : (int)date.DayOfWeek;
        return date.AddDays(1 - dayOfWeek);
    }

    private static IQueryable<ExamRoomSession> ApplyMixedResourceFilter(
        IQueryable<ExamRoomSession> source,
        TimetableResourceType resourceType,
        Guid resourceId,
        Guid? studentId)
    {
        switch (resourceType)
        {
            case TimetableResourceType.Classroom:
                return source.Where(link =>
                    link.ExamRoom!.ClassroomId == resourceId);
            case TimetableResourceType.Teacher:
                return source.Where(link =>
                    link.ExamRoom!.Invigilators.Any(i =>
                        i.TeacherId == resourceId) ||
                    link.ExamSession!.TeachingTask!.Teachers.Any(t =>
                        t.TeacherId == resourceId));
            case TimetableResourceType.Class:
                if (studentId.HasValue)
                {
                    return source.Where(link =>
                        link.ExamRoom!.Seats.Any(s =>
                            s.StudentId == studentId.Value &&
                            s.ExamSessionId == link.ExamSessionId));
                }
                return source.Where(link =>
                    link.ExamSession!.TeachingTask!.Classes.Any(c =>
                        c.AdministrativeClassId == resourceId));
            default:
                return source;
        }
    }

    private IQueryable<ExamSession> ApplyLegacyResourceFilter(
        IQueryable<ExamSession> source,
        TimetableResourceType resourceType,
        Guid resourceId,
        Guid? studentId)
    {
        switch (resourceType)
        {
            case TimetableResourceType.Classroom:
                return source.Where(x => x.ClassroomId == resourceId);
            case TimetableResourceType.Teacher:
                return source.Where(x =>
                    x.Invigilators.Any(i => i.TeacherId == resourceId) ||
                    x.TeachingTask!.Teachers.Any(t => t.TeacherId == resourceId));
            case TimetableResourceType.Class:
                if (studentId.HasValue)
                {
                    return source.Where(x =>
                        x.TeachingTask!.Classes.Any(c =>
                            c.AdministrativeClassId == resourceId) ||
                        db.CourseEnrollments.Any(e =>
                            e.StudentId == studentId.Value &&
                            e.Status == CourseEnrollmentStatus.Enrolled &&
                            e.CourseSelectionOffering!.TeachingTaskId ==
                                x.TeachingTaskId));
                }
                return source.Where(x =>
                    x.TeachingTask!.Classes.Any(c =>
                        c.AdministrativeClassId == resourceId));
            default:
                return source;
        }
    }

    private static TimetableEntryDto MapToEntryDto(
        ExamSessionProjection x,
        IReadOnlyDictionary<int, TimetableSlotDto> slotLookup)
    {
        var dayOfWeek = x.ExamDate.DayOfWeek == 0
            ? 7
            : (int)x.ExamDate.DayOfWeek;
        var teacherNames = x.InvigilatorNames.Count > 0
            ? x.TeacherNames.Concat(
                new[] { "监考：" + string.Join("、", x.InvigilatorNames) })
            : x.TeacherNames;
        return new TimetableEntryDto(
            x.ExamRoomId,
            x.TeachingTaskId,
            x.TaskNumber,
            x.TaskName,
            x.CourseCode,
            x.CourseName,
            teacherNames,
            x.ClassNames,
            x.ClassroomName,
            x.BuildingName,
            x.CampusName,
            dayOfWeek,
            x.StartPeriod,
            x.PeriodCount,
            1, 1,
            WeekPattern.All,
            x.Notes,
            true,
            x.PlanName,
            x.ExamDate,
            x.UpdatedAt);
    }

    private static TimetableEntryDto MapClassExamEntryDto(
        IEnumerable<ExamSessionProjection> sessions,
        IReadOnlyDictionary<int, TimetableSlotDto> slotLookup)
    {
        var rooms = sessions.ToList();
        var first = rooms[0];
        var roomCount = rooms
            .Select(x => x.ExamRoomId)
            .Distinct()
            .Count();
        var buildingNames = rooms
            .Select(x => x.BuildingName)
            .Distinct()
            .ToList();
        var campusNames = rooms
            .Select(x => x.CampusName)
            .Distinct()
            .ToList();
        var entry = MapToEntryDto(first, slotLookup);
        return entry with
        {
            Id = first.ExamSessionId,
            TeacherNames = first.TeacherNames,
            ClassroomName = roomCount == 1
                ? first.ClassroomName
                : $"分散至 {roomCount} 个考场",
            BuildingName = buildingNames.Count == 1 ? buildingNames[0] : null,
            CampusName = campusNames.Count == 1 ? campusNames[0] : null,
            UpdatedAt = rooms.Max(x => x.UpdatedAt),
            ExamRoomCount = roomCount
        };
    }

    private sealed record ExamSessionProjection(
        Guid ExamSessionId,
        Guid ExamRoomId,
        Guid TeachingTaskId,
        string TaskNumber,
        string TaskName,
        string CourseCode,
        string CourseName,
        List<string> TeacherNames,
        List<string> ClassNames,
        string ClassroomName,
        string BuildingName,
        string CampusName,
        DateOnly ExamDate,
        int StartPeriod,
        int PeriodCount,
        string PlanName,
        List<string> InvigilatorNames,
        string? Notes,
        DateTime UpdatedAt);

    private sealed record ExperimentSessionProjection(
        Guid Id,
        Guid ExperimentProjectId,
        string ProjectCode,
        string ProjectName,
        ExperimentArrangementMode ArrangementMode,
        Guid TeachingTaskId,
        string TaskNumber,
        string TaskName,
        string CourseCode,
        string CourseName,
        List<string> TeacherNames,
        List<string> ClassNames,
        string ClassroomName,
        string BuildingName,
        string CampusName,
        DateOnly SessionDate,
        int StartPeriod,
        int PeriodCount,
        string? Notes,
        DateTime UpdatedAt);
}

public enum TimetableResourceType
{
    Class = 1,
    Teacher = 2,
    Classroom = 3
}

public sealed record TimetableData(
    TimetableTermDto Term,
    TimetableSubjectDto Subject,
    TimetableSubjectDto? Class,
    TimetableStudentDto? Student,
    TimetablePlanDto? Plan,
    IReadOnlyList<TimetableSlotDto> Slots,
    IReadOnlyList<TimetableEntryDto> Entries,
    IReadOnlyList<FlexibleCourseDto> FlexibleCourses,
    IReadOnlyList<TimetableEntryDto> ExamEntries)
{
    public IReadOnlyList<TimetableEntryDto> ExperimentEntries { get; init; } = [];
}

public sealed record TimetableTermDto(
    Guid Id,
    string Name,
    string AcademicYear,
    TermSeason Season,
    DateOnly StartDate,
    DateOnly EndDate,
    bool IsCurrent);

public sealed record TimetableSubjectDto(
    Guid Id,
    string Code,
    string Name,
    TimetableResourceType Type,
    int? Grade,
    Guid? MajorId,
    string? MajorName,
    Guid? CollegeId,
    string? CollegeName,
    Guid? CampusId,
    Guid? BuildingId,
    string? Description);

public sealed record TimetableStudentDto(string StudentNumber, string Name);

public sealed record TimetablePlanDto(
    Guid Id,
    string Name,
    string Version,
    SchedulePlanStatus Status,
    DateTime? PublishedAt,
    DateTime UpdatedAt);

public sealed record TimetableSlotDto(
    int PeriodNumber,
    string Name,
    TimeOnly StartsAt,
    TimeOnly EndsAt);

public sealed record TimetableEntryDto(
    Guid Id,
    Guid TeachingTaskId,
    string TaskNumber,
    string TaskName,
    string CourseCode,
    string CourseName,
    IEnumerable<string> TeacherNames,
    IEnumerable<string> ClassNames,
    string ClassroomName,
    string? BuildingName,
    string? CampusName,
    int DayOfWeek,
    int StartPeriod,
    int PeriodCount,
    int StartWeek,
    int EndWeek,
    WeekPattern WeekPattern,
    string? Notes,
    bool IsExam = false,
    string? ExamPlanName = null,
    DateOnly? ExamDate = null,
    DateTime UpdatedAt = default,
    bool IsExperiment = false,
    Guid? ExperimentProjectId = null,
    string? ExperimentProjectCode = null,
    string? ExperimentProjectName = null,
    DateOnly? ExperimentDate = null,
    ExperimentArrangementMode? ExperimentArrangementMode = null,
    ScheduleEntryKind Kind = ScheduleEntryKind.Lecture,
    int ExamRoomCount = 1);

public sealed record FlexibleCourseDto(
    Guid Id,
    string TaskNumber,
    string Name,
    string CourseCode,
    string CourseName,
    decimal Credits,
    int TotalHours,
    int StartWeek,
    int EndWeek,
    int WeeklyHours,
    IEnumerable<string> TeacherNames,
    IEnumerable<string> ClassNames,
    string? Notes,
    DateTime UpdatedAt = default);
