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

namespace Jiaowu.Api.Infrastructure.Exams;

public sealed class ExamArrangementService(AppDbContext db)
{
    private sealed record RoomGroupKey(
        Guid CourseId,
        DateOnly ExamDate,
        int StartPeriod,
        int PeriodCount,
        string RequiredBuildingIdsKey);

    private sealed record RoomOccupancy(
        Guid ClassroomId,
        DateTime StartsAt,
        DateTime EndsAt);

    private sealed record InvigilatorOccupancy(
        Guid TeacherId,
        DateTime StartsAt,
        DateTime EndsAt);

    private sealed record CandidateSeat(
        Guid ExamSessionId,
        Guid StudentId,
        string StudentNumber);

    public async Task<ExamArrangementResult> ArrangeAsync(
        Guid planId,
        IReadOnlyCollection<Guid>? requestedSessionIds,
        bool assignClassrooms,
        bool assignInvigilators,
        CancellationToken cancellationToken)
    {
        if (!assignClassrooms && !assignInvigilators)
            return ExamArrangementResult.Fail("请至少选择分配考场或分配监考教师。");

        var plan = await db.ExamPlans
            .Include(x => x.AcademicTerm)
            .Include(x => x.Sessions)
                .ThenInclude(x => x.Invigilators)
            .Include(x => x.Sessions)
                .ThenInclude(x => x.TeachingTask)
                    .ThenInclude(x => x!.Teachers)
            .Include(x => x.Sessions)
                .ThenInclude(x => x.TeachingTask)
                    .ThenInclude(x => x!.Course)
            .Include(x => x.Rooms)
                .ThenInclude(x => x.SessionLinks)
            .Include(x => x.Rooms)
                .ThenInclude(x => x.Seats)
            .Include(x => x.Rooms)
                .ThenInclude(x => x.Invigilators)
            .AsSplitQuery()
            .FirstOrDefaultAsync(x => x.Id == planId, cancellationToken);

        if (plan is null)
            return ExamArrangementResult.Fail("考试计划不存在。");
        if (plan.Status != ExamPlanStatus.Draft)
            return ExamArrangementResult.Fail("只有草稿状态的考试计划可以自动编排。");

        var requestedIds = (requestedSessionIds ?? [])
            .Distinct()
            .ToHashSet();
        if (requestedIds.Count > 100)
            return ExamArrangementResult.Fail("一次最多处理100个考试场次。");
        if (requestedIds.Count > 0 &&
            plan.Sessions.Count(x => requestedIds.Contains(x.Id)) != requestedIds.Count)
            return ExamArrangementResult.Fail("所选场次不存在或不属于当前考试计划。");

        var timeSlots = await db.ScheduleTimeSlots.AsNoTracking()
            .Where(x => x.AcademicTermId == plan.AcademicTermId && x.IsEnabled)
            .OrderBy(x => x.PeriodNumber)
            .ToListAsync(cancellationToken);
        if (timeSlots.Count == 0)
            return ExamArrangementResult.Fail("当前学期未配置上课时间表，无法计算考试时间段。");

        var timeSlotLookup = timeSlots.ToDictionary(x => x.PeriodNumber);
        foreach (var session in plan.Sessions)
            ComputeTimesFromSlots(session, timeSlotLookup);

        var explicitlySelected = plan.Sessions
            .Where(x => requestedIds.Count == 0 || requestedIds.Contains(x.Id))
            .ToList();
        if (explicitlySelected.Count == 0)
            return ExamArrangementResult.Fail("没有可处理的考试场次。");

        var selectedKeys = explicitlySelected
            .Select(GroupKey)
            .ToHashSet();
        var sessions = plan.Sessions
            .Where(x => selectedKeys.Contains(GroupKey(x)))
            .OrderBy(x => x.ExamDate)
            .ThenBy(x => x.StartPeriod)
            .ThenBy(x => x.TeachingTask!.Course!.Code)
            .ThenBy(x => x.TeachingTask!.TaskNumber)
            .ToList();
        var sessionIds = sessions.Select(x => x.Id).ToHashSet();

        var roster = await TeachingTaskRosterQuery.LoadForTasksAsync(
            db,
            sessions.Select(x => x.TeachingTaskId),
            cancellationToken);
        var sessionByTaskId = sessions
            .GroupBy(x => x.TeachingTaskId)
            .ToDictionary(x => x.Key, x => x.First());

        var replacedRooms = assignClassrooms
            ? plan.Rooms
                .Where(room => room.SessionLinks.Any(link =>
                    sessionIds.Contains(link.ExamSessionId)))
                .ToList()
            : [];
        var replacedRoomIds = replacedRooms.Select(x => x.Id).ToHashSet();
        if (replacedRooms.Count > 0)
            db.ExamRooms.RemoveRange(replacedRooms);

        var occupiedRooms = await LoadOccupiedRoomsAsync(
            plan,
            sessionIds,
            replacedRoomIds,
            cancellationToken);
        var rooms = await db.Classrooms.AsNoTracking()
            .Include(x => x.Building)
            .Where(x => x.IsEnabled)
            .OrderBy(x => x.Building!.Name)
            .ThenBy(x => x.Name)
            .ToListAsync(cancellationToken);

        var targetRooms = new List<ExamRoomAssignment>();
        var messages = new List<string>();
        var seatedStudents = 0;
        var unavailableStudents = 0;

        if (assignClassrooms)
        {
            foreach (var session in sessions)
            {
                session.ClassroomId = null;
                if (session.Invigilators.Count > 0)
                {
                    db.ExamSessionInvigilators.RemoveRange(session.Invigilators);
                    session.Invigilators.Clear();
                }
            }

            foreach (var group in sessions
                         .GroupBy(GroupKey)
                         .OrderBy(x => x.Key.ExamDate)
                         .ThenBy(x => x.Key.StartPeriod)
                         .ThenBy(x => x.First().TeachingTask!.Course!.Code))
            {
                var groupSessions = group.ToList();
                var candidates = InterleaveCandidates(
                    groupSessions,
                    roster,
                    sessionByTaskId);
                if (candidates.Count == 0)
                {
                    messages.Add(
                        $"“{groupSessions[0].TeachingTask!.Course!.Name}”没有有效考生。");
                    continue;
                }

                var availableRooms = rooms
                    .Where(room =>
                        (group.Key.RequiredBuildingIdsKey.Length == 0 ||
                         group.Key.RequiredBuildingIdsKey
                             .Split(',', StringSplitOptions.RemoveEmptyEntries)
                             .Contains(room.BuildingId.ToString())) &&
                        occupiedRooms.All(occupied =>
                            occupied.ClassroomId != room.Id ||
                            !ExamConflictRules.TimeOverlaps(
                                occupied.StartsAt,
                                occupied.EndsAt,
                                groupSessions[0].StartsAt,
                                groupSessions[0].EndsAt)))
                    .ToList();
                var selectedRooms = SelectRooms(
                    availableRooms,
                    candidates.Count);
                if (selectedRooms.Sum(x => x.Capacity / 2) < candidates.Count)
                {
                    unavailableStudents += candidates.Count;
                    messages.Add(
                        $"“{groupSessions[0].TeachingTask!.Course!.Name}”缺少足够考场容量，" +
                        $"需 {candidates.Count} 座、可用 {selectedRooms.Sum(x => x.Capacity / 2)} 座。");
                    continue;
                }

                var offset = 0;
                foreach (var classroom in selectedRooms)
                {
                    var roomCandidates = candidates
                        .Skip(offset)
                        .Take(classroom.Capacity / 2)
                        .ToList();
                    if (roomCandidates.Count == 0) break;
                    offset += roomCandidates.Count;

                    var room = new ExamRoomAssignment
                    {
                        ExamPlanId = plan.Id,
                        CourseId = group.Key.CourseId,
                        ClassroomId = classroom.Id,
                        ExamDate = group.Key.ExamDate,
                        StartPeriod = group.Key.StartPeriod,
                        PeriodCount = group.Key.PeriodCount,
                        StartsAt = groupSessions[0].StartsAt,
                        EndsAt = groupSessions[0].EndsAt,
                        RequiredInvigilatorCount =
                            groupSessions.Max(x => x.RequiredInvigilatorCount),
                        SessionLinks = roomCandidates
                            .Select(x => x.ExamSessionId)
                            .Distinct()
                            .Select(sessionId => new ExamRoomSession
                            {
                                ExamSessionId = sessionId
                            })
                            .ToList(),
                        Seats = roomCandidates
                            .Select((candidate, index) =>
                                new ExamSeatAssignment
                                {
                                    ExamSessionId = candidate.ExamSessionId,
                                    StudentId = candidate.StudentId,
                                    SeatNumber = index + 1
                                })
                            .ToList()
                    };
                    db.ExamRooms.Add(room);
                    targetRooms.Add(room);
                    occupiedRooms.Add(new RoomOccupancy(
                        classroom.Id,
                        room.StartsAt,
                        room.EndsAt));
                    seatedStudents += roomCandidates.Count;
                }

                messages.Add(
                    $"“{groupSessions[0].TeachingTask!.Course!.Name}”" +
                    $"{groupSessions.Count}个教学班混排至{selectedRooms.Count}个考场。");
            }
        }
        else
        {
            targetRooms.AddRange(plan.Rooms.Where(room =>
                room.SessionLinks.Any(link =>
                    sessionIds.Contains(link.ExamSessionId))));
            var linkedSessionIds = targetRooms
                .SelectMany(x => x.SessionLinks)
                .Select(x => x.ExamSessionId)
                .ToHashSet();
            foreach (var session in sessions.Where(x =>
                         !linkedSessionIds.Contains(x.Id) &&
                         x.ClassroomId.HasValue))
            {
                var legacyRoom = CreateLegacyRoom(
                    plan,
                    session,
                    roster.Where(x =>
                        x.TeachingTaskId == session.TeachingTaskId));
                db.ExamRooms.Add(legacyRoom);
                targetRooms.Add(legacyRoom);
                session.ClassroomId = null;
                if (session.Invigilators.Count > 0)
                {
                    db.ExamSessionInvigilators.RemoveRange(session.Invigilators);
                    session.Invigilators.Clear();
                }
            }
            seatedStudents = targetRooms.Sum(x => x.Seats.Count);
        }

        var assignedInvigilators = 0;
        var unavailableInvigilators = 0;
        if (assignInvigilators)
        {
            if (targetRooms.Count == 0)
                return ExamArrangementResult.Fail("尚未生成实际考场，请先分配考场。");

            var occupiedInvigilators = plan.Rooms
                .Where(x => !replacedRoomIds.Contains(x.Id) &&
                            !targetRooms.Any(target => target.Id == x.Id))
                .SelectMany(room => room.Invigilators.Select(item =>
                    new InvigilatorOccupancy(
                        item.TeacherId,
                        room.StartsAt,
                        room.EndsAt)))
                .Concat(plan.Sessions
                    .Where(x => !sessionIds.Contains(x.Id))
                    .SelectMany(session => session.Invigilators.Select(item =>
                        new InvigilatorOccupancy(
                            item.TeacherId,
                            session.StartsAt,
                            session.EndsAt))))
                .ToList();

            foreach (var room in targetRooms
                         .OrderBy(x => x.ExamDate)
                         .ThenBy(x => x.StartPeriod)
                         .ThenBy(x => x.ClassroomId))
            {
                var needed = room.RequiredInvigilatorCount -
                             room.Invigilators.Count;
                if (needed <= 0) continue;

                var linkedSessionIds = room.SessionLinks
                    .Select(x => x.ExamSessionId)
                    .ToHashSet();
                var excludedTeacherIds = sessions
                    .Where(x => linkedSessionIds.Contains(x.Id))
                    .SelectMany(x => x.TeachingTask!.Teachers)
                    .Select(x => x.TeacherId)
                    .ToHashSet();
                var teachers = await FindInvigilatorsAsync(
                    room,
                    needed,
                    excludedTeacherIds,
                    occupiedInvigilators,
                    cancellationToken);
                foreach (var teacher in teachers)
                {
                    room.Invigilators.Add(new ExamRoomInvigilator
                    {
                        TeacherId = teacher.Id
                    });
                    occupiedInvigilators.Add(new InvigilatorOccupancy(
                        teacher.Id,
                        room.StartsAt,
                        room.EndsAt));
                    assignedInvigilators++;
                }

                if (teachers.Count < needed)
                    unavailableInvigilators += needed - teachers.Count;
            }
        }

        await db.SaveChangesAsync(cancellationToken);

        var expandedCount = sessions.Count - explicitlySelected.Count;
        var detail = messages.Count > 0
            ? $" 详情：{string.Join("；", messages.Take(10))}"
            : "";
        return new ExamArrangementResult(
            true,
            $"{sessions.Count}个教学班场次处理完成，生成{targetRooms.Count}个实际考场，" +
            $"安排{seatedStudents}名考生、{assignedInvigilators}名监考教师。" +
            (expandedCount > 0
                ? $" 为保持混排完整性，自动包含同组{expandedCount}个场次。"
                : "") +
            (unavailableStudents > 0
                ? $" {unavailableStudents}名考生尚未安排考场。"
                : "") +
            (unavailableInvigilators > 0
                ? $" 仍缺{unavailableInvigilators}名监考教师。"
                : "") +
            detail);
    }

    private async Task<List<RoomOccupancy>> LoadOccupiedRoomsAsync(
        ExamPlan plan,
        IReadOnlySet<Guid> targetSessionIds,
        IReadOnlySet<Guid> replacedRoomIds,
        CancellationToken cancellationToken)
    {
        var occupied = plan.Rooms
            .Where(x => !replacedRoomIds.Contains(x.Id))
            .Select(x => new RoomOccupancy(
                x.ClassroomId,
                x.StartsAt,
                x.EndsAt))
            .ToList();
        occupied.AddRange(plan.Sessions
            .Where(x =>
                !targetSessionIds.Contains(x.Id) &&
                x.ClassroomId.HasValue)
            .Select(x => new RoomOccupancy(
                x.ClassroomId!.Value,
                x.StartsAt,
                x.EndsAt)));

        var externalRooms = await db.ExamRooms.AsNoTracking()
            .Where(x =>
                x.ExamPlanId != plan.Id &&
                x.ExamPlan!.Status != ExamPlanStatus.Archived)
            .Select(x => new RoomOccupancy(
                x.ClassroomId,
                x.StartsAt,
                x.EndsAt))
            .ToListAsync(cancellationToken);
        occupied.AddRange(externalRooms);

        var externalLegacyRooms = await db.ExamSessions.AsNoTracking()
            .Where(x =>
                x.ExamPlanId != plan.Id &&
                x.ExamPlan!.Status != ExamPlanStatus.Archived &&
                x.ClassroomId != null)
            .Select(x => new RoomOccupancy(
                x.ClassroomId!.Value,
                x.StartsAt,
                x.EndsAt))
            .ToListAsync(cancellationToken);
        occupied.AddRange(externalLegacyRooms);
        return occupied;
    }

    private async Task<List<Teacher>> FindInvigilatorsAsync(
        ExamRoomAssignment room,
        int needed,
        HashSet<Guid> excludedTeacherIds,
        List<InvigilatorOccupancy> occupied,
        CancellationToken cancellationToken)
    {
        var busyTeacherIds = occupied
            .Where(x => ExamConflictRules.TimeOverlaps(
                x.StartsAt,
                x.EndsAt,
                room.StartsAt,
                room.EndsAt))
            .Select(x => x.TeacherId)
            .ToHashSet();

        var databaseBusyIds = await db.ExamRoomInvigilators.AsNoTracking()
            .Where(x =>
                x.ExamRoomId != room.Id &&
                x.ExamRoom!.ExamPlan!.Status != ExamPlanStatus.Archived &&
                x.ExamRoom.StartsAt < room.EndsAt &&
                room.StartsAt < x.ExamRoom.EndsAt)
            .Select(x => x.TeacherId)
            .ToListAsync(cancellationToken);
        foreach (var teacherId in databaseBusyIds)
            busyTeacherIds.Add(teacherId);

        var legacyBusyIds = await db.ExamSessionInvigilators.AsNoTracking()
            .Where(x =>
                x.ExamSession!.ExamPlan!.Status != ExamPlanStatus.Archived &&
                x.ExamSession.StartsAt < room.EndsAt &&
                room.StartsAt < x.ExamSession.EndsAt)
            .Select(x => x.TeacherId)
            .ToListAsync(cancellationToken);
        foreach (var teacherId in legacyBusyIds)
            busyTeacherIds.Add(teacherId);
        foreach (var teacherId in excludedTeacherIds)
            busyTeacherIds.Add(teacherId);

        var candidates = await InvigilatorCandidateQuery
            .Create(db, busyTeacherIds)
            .ToListAsync(cancellationToken);
        return candidates
            .OrderBy(_ => Random.Shared.Next())
            .Take(needed)
            .ToList();
    }

    private static List<Classroom> SelectRooms(
        IReadOnlyCollection<Classroom> availableRooms,
        int candidateCount)
    {
        var remainingRooms = availableRooms.ToList();
        var selected = new List<Classroom>();
        var remainingSeats = candidateCount;
        while (remainingSeats > 0 && remainingRooms.Count > 0)
        {
            var room = remainingRooms
                           .Where(x => x.Capacity / 2 >= remainingSeats)
                           .OrderBy(x => x.Capacity / 2)
                           .ThenBy(x => x.Name)
                           .FirstOrDefault()
                       ?? remainingRooms
                           .OrderByDescending(x => x.Capacity / 2)
                           .ThenBy(x => x.Name)
                           .First();
            selected.Add(room);
            remainingRooms.Remove(room);
            remainingSeats -= room.Capacity / 2;
        }
        return selected;
    }

    private static List<CandidateSeat> InterleaveCandidates(
        IReadOnlyCollection<ExamSession> sessions,
        IReadOnlyCollection<TeachingTaskRosterEntry> roster,
        IReadOnlyDictionary<Guid, ExamSession> sessionByTaskId)
    {
        var sessionIds = sessions.Select(x => x.Id).ToHashSet();
        var queues = roster
            .Where(x =>
                sessionByTaskId.TryGetValue(
                    x.TeachingTaskId,
                    out var session) &&
                sessionIds.Contains(session.Id))
            .GroupBy(x => x.TeachingTaskId)
            .OrderBy(x => sessionByTaskId[x.Key].TeachingTask!.TaskNumber)
            .Select(group => new Queue<CandidateSeat>(
                group.OrderBy(x => x.StudentNumber)
                    .Select(x => new CandidateSeat(
                        sessionByTaskId[x.TeachingTaskId].Id,
                        x.StudentId,
                        x.StudentNumber))))
            .ToList();

        var result = new List<CandidateSeat>();
        var assignedStudentIds = new HashSet<Guid>();
        while (queues.Any(x => x.Count > 0))
        {
            foreach (var queue in queues)
            {
                while (queue.Count > 0)
                {
                    var candidate = queue.Dequeue();
                    if (!assignedStudentIds.Add(candidate.StudentId))
                        continue;
                    result.Add(candidate);
                    break;
                }
            }
        }
        return result;
    }

    private static ExamRoomAssignment CreateLegacyRoom(
        ExamPlan plan,
        ExamSession session,
        IEnumerable<TeachingTaskRosterEntry> roster)
    {
        var students = roster
            .OrderBy(x => x.StudentNumber)
            .ToList();
        return new ExamRoomAssignment
        {
            ExamPlanId = plan.Id,
            CourseId = session.TeachingTask!.CourseId,
            ClassroomId = session.ClassroomId!.Value,
            ExamDate = session.ExamDate,
            StartPeriod = session.StartPeriod,
            PeriodCount = session.PeriodCount,
            StartsAt = session.StartsAt,
            EndsAt = session.EndsAt,
            RequiredInvigilatorCount = session.RequiredInvigilatorCount,
            SessionLinks =
            [
                new ExamRoomSession
                {
                    ExamSessionId = session.Id
                }
            ],
            Seats = students.Select((student, index) =>
                new ExamSeatAssignment
                {
                    ExamSessionId = session.Id,
                    StudentId = student.StudentId,
                    SeatNumber = index + 1
                }).ToList(),
            Invigilators = session.Invigilators.Select(x =>
                new ExamRoomInvigilator
                {
                    TeacherId = x.TeacherId
                }).ToList()
        };
    }

    private static RoomGroupKey GroupKey(ExamSession session)
    {
        var buildingIds = ParseBuildingIds(session.RequiredBuildingIds);
        if (session.RequiredBuildingId.HasValue)
            buildingIds.Add(session.RequiredBuildingId.Value);
        var key = buildingIds.Count == 0
            ? string.Empty
            : string.Join(",", buildingIds.OrderBy(x => x));
        return new(
            session.TeachingTask!.CourseId,
            session.ExamDate,
            session.StartPeriod,
            session.PeriodCount,
            key);
    }

    private static HashSet<Guid> ParseBuildingIds(string? json)
    {
        if (string.IsNullOrWhiteSpace(json))
            return [];
        try
        {
            return System.Text.Json.JsonSerializer.Deserialize<HashSet<Guid>>(json) ?? [];
        }
        catch
        {
            return [];
        }
    }

    private static void ComputeTimesFromSlots(
        ExamSession session,
        IReadOnlyDictionary<int, ScheduleTimeSlot> timeSlotLookup)
    {
        var startSlot = timeSlotLookup.GetValueOrDefault(session.StartPeriod);
        var endSlot = timeSlotLookup.GetValueOrDefault(
            session.StartPeriod + session.PeriodCount - 1);
        if (startSlot is null || endSlot is null) return;

        session.StartsAt = session.ExamDate.ToDateTime(
            startSlot.StartsAt,
            DateTimeKind.Utc);
        session.EndsAt = session.ExamDate.ToDateTime(
            endSlot.EndsAt,
            DateTimeKind.Utc);
    }
}

public sealed record ExamArrangementResult(bool Success, string Message)
{
    public static ExamArrangementResult Fail(string message) =>
        new(false, message);
}
