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

namespace Jiaowu.Api.Infrastructure.Timetables;

public sealed class ClassroomReservationAvailabilityService(AppDbContext db)
{
    public async Task<HashSet<Guid>> GetOccupiedClassroomIdsAsync(
        AcademicTerm term,
        DateOnly reservationDate,
        int startPeriod,
        int periodCount,
        Guid? excludedReservationId,
        CancellationToken cancellationToken)
    {
        var occupiedIds = new HashSet<Guid>();
        var (week, dayOfWeek) = ResolveTeachingWeek(term, reservationDate);

        var hasProjection = await db.PublishedScheduleOccurrences.AsNoTracking()
            .AnyAsync(entry => entry.AcademicTermId == term.Id, cancellationToken);
        if (hasProjection)
        {
            var projectedRoomIds = await db.PublishedScheduleOccurrences.AsNoTracking()
                .Where(entry => entry.AcademicTermId == term.Id && entry.Week == week &&
                    entry.DayOfWeek == dayOfWeek && entry.ClassroomId.HasValue &&
                    entry.StartPeriod < startPeriod + periodCount &&
                    startPeriod < entry.StartPeriod + entry.PeriodCount)
                .Select(entry => entry.ClassroomId!.Value)
                .ToListAsync(cancellationToken);
            occupiedIds.UnionWith(projectedRoomIds);
        }
        else
        {
            var scheduleEntries = await db.ScheduleEntries.AsNoTracking()
                .Where(entry => entry.ClassroomId.HasValue &&
                    entry.SchedulePlan!.AcademicTermId == term.Id &&
                    entry.SchedulePlan.Status == SchedulePlanStatus.Published &&
                    entry.DayOfWeek == dayOfWeek && entry.StartWeek <= week &&
                    entry.EndWeek >= week && entry.StartPeriod < startPeriod + periodCount &&
                    startPeriod < entry.StartPeriod + entry.PeriodCount)
                .Select(entry => new { entry.ClassroomId, entry.WeekPattern, entry.StartPeriod, entry.PeriodCount })
                .ToListAsync(cancellationToken);
            foreach (var entry in scheduleEntries.Where(entry =>
                         FreeClassroomRules.MatchesWeek(entry.WeekPattern, week) &&
                         FreeClassroomRules.PeriodsOverlap(startPeriod, periodCount, entry.StartPeriod, entry.PeriodCount)))
                occupiedIds.Add(entry.ClassroomId!.Value);
        }

        var examRoomIds = await db.ExamSessions.AsNoTracking()
            .Where(session =>
                session.ClassroomId.HasValue &&
                session.ExamPlan!.AcademicTermId == term.Id &&
                session.ExamPlan.Status == ExamPlanStatus.Published &&
                session.ExamDate == reservationDate &&
                session.StartPeriod < startPeriod + periodCount &&
                startPeriod < session.StartPeriod + session.PeriodCount)
            .Select(session => session.ClassroomId!.Value)
            .ToListAsync(cancellationToken);
        occupiedIds.UnionWith(examRoomIds);

        var mixedExamRoomIds = await db.ExamRooms.AsNoTracking()
            .Where(room =>
                room.ExamPlan!.AcademicTermId == term.Id &&
                room.ExamPlan.Status == ExamPlanStatus.Published &&
                room.ExamDate == reservationDate &&
                room.StartPeriod < startPeriod + periodCount &&
                startPeriod < room.StartPeriod + room.PeriodCount)
            .Select(room => room.ClassroomId)
            .ToListAsync(cancellationToken);
        occupiedIds.UnionWith(mixedExamRoomIds);

        var makeupExamRoomIds = await db.MakeupExamSessions.AsNoTracking()
            .Where(session =>
                session.ClassroomId.HasValue &&
                session.MakeupExamPlan!.AcademicTermId == term.Id &&
                session.MakeupExamPlan.Status == MakeupExamPlanStatus.Published &&
                session.ExamDate == reservationDate &&
                session.StartPeriod < startPeriod + periodCount &&
                startPeriod < session.StartPeriod + session.PeriodCount)
            .Select(session => session.ClassroomId!.Value)
            .ToListAsync(cancellationToken);
        occupiedIds.UnionWith(makeupExamRoomIds);

        var experimentRoomIds = await db.ExperimentSessions.AsNoTracking()
            .Where(session =>
                session.ExperimentProject!.TeachingTask!.AcademicTermId ==
                    term.Id &&
                session.ExperimentProject.Status !=
                    ExperimentProjectStatus.Closed &&
                session.Status == ExperimentSessionStatus.Scheduled &&
                session.SessionDate == reservationDate &&
                session.StartPeriod < startPeriod + periodCount &&
                startPeriod < session.StartPeriod + session.PeriodCount)
            .Select(session => session.ClassroomId)
            .ToListAsync(cancellationToken);
        occupiedIds.UnionWith(experimentRoomIds);

        var reservationQuery = db.ClassroomReservations.AsNoTracking()
            .Where(reservation =>
                reservation.AcademicTermId == term.Id &&
                reservation.Status == ClassroomReservationStatus.Approved &&
                reservation.ReservationDate == reservationDate &&
                reservation.StartPeriod < startPeriod + periodCount &&
                startPeriod <
                    reservation.StartPeriod + reservation.PeriodCount);
        if (excludedReservationId.HasValue)
            reservationQuery = reservationQuery.Where(reservation =>
                reservation.Id != excludedReservationId.Value);
        var reservationRoomIds = await reservationQuery
            .Select(reservation => reservation.ClassroomId)
            .ToListAsync(cancellationToken);
        occupiedIds.UnionWith(reservationRoomIds);
        return occupiedIds;
    }

    public static (int Week, int DayOfWeek) ResolveTeachingWeek(
        AcademicTerm term,
        DateOnly date)
    {
        var termStartDay = (int)term.StartDate.DayOfWeek;
        var daysSinceMonday = (termStartDay + 6) % 7;
        var firstWeekMonday = term.StartDate.AddDays(-daysSinceMonday);
        var week = (date.DayNumber - firstWeekMonday.DayNumber) / 7 + 1;
        var dayOfWeek = ((int)date.DayOfWeek + 6) % 7 + 1;
        return (week, dayOfWeek);
    }
}
