using System.Text.Json;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Infrastructure.Exams;

public sealed class MakeupExamAutoJobProcessor(
    AppDbContext db,
    MakeupExamEligibilityService eligibilityService,
    ILogger<MakeupExamAutoJobProcessor> logger)
{
    public async Task ProcessAsync(Guid jobId, CancellationToken stoppingToken)
    {
        try
        {
            var job = await db.MakeupExamAutoJobs
                .FirstOrDefaultAsync(x => x.Id == jobId, stoppingToken);
            if (job is null ||
                job.Status is MakeupExamAutoJobStatus.Succeeded
                    or MakeupExamAutoJobStatus.Failed)
            {
                return;
            }

            var plan = await db.MakeupExamPlans
                .Include(x => x.Sessions)
                .FirstOrDefaultAsync(x => x.Id == job.MakeupExamPlanId, stoppingToken);
            if (plan is null || plan.Status != MakeupExamPlanStatus.Draft)
                throw new InvalidOperationException("补考计划草稿不存在或已不允许修改。");

            job.Status = MakeupExamAutoJobStatus.Running;
            job.StartedAt = DateTime.UtcNow;
            job.CompletedAt = null;
            job.ErrorMessage = null;
            job.MessagesJson = null;
            await db.SaveChangesAsync(stoppingToken);

            // Get time slots for the term
            var timeSlots = await db.ScheduleTimeSlots.AsNoTracking()
                .Where(x => x.AcademicTermId == plan.AcademicTermId && x.IsEnabled)
                .OrderBy(x => x.PeriodNumber)
                .ToListAsync(stoppingToken);

            if (timeSlots.Count == 0)
                throw new InvalidOperationException("当前学期未配置上课时间表。");

            var slotLookup = timeSlots.ToDictionary(x => x.PeriodNumber);

            // Get all published teaching tasks for this term with grade sheets
            var tasks = await db.TeachingTasks.AsNoTracking()
                .Where(x => x.AcademicTermId == plan.AcademicTermId &&
                            x.Status == TeachingTaskStatus.Published)
                .Select(x => new { x.Id, x.TaskNumber, x.Name, CourseName = x.Course!.Name })
                .OrderBy(x => x.TaskNumber)
                .ToListAsync(stoppingToken);

            job.TotalCourses = tasks.Count;
            job.ProcessedCourses = 0;
            job.CreatedSessions = 0;
            job.EnrolledStudents = 0;
            await db.SaveChangesAsync(stoppingToken);

            var messages = new List<string>();
            var existingTaskIds = plan.Sessions.Select(s => s.TeachingTaskId).ToHashSet();

            // Get available classrooms for mixed-room assignment
            var allRooms = await db.Classrooms.AsNoTracking()
                .Where(x => x.IsEnabled)
                .OrderBy(x => x.Capacity)
                .Select(x => new { x.Id, x.Name, x.Capacity, x.BuildingId })
                .ToListAsync(stoppingToken);

            // Get available teachers
            var allTeachers = await db.Teachers.AsNoTracking()
                .Where(x => x.Status == TeacherStatus.Active)
                .Select(x => new { x.Id, x.Name })
                .ToListAsync(stoppingToken);

            // Auto-assign dates: spread sessions across available dates
            // Start from tomorrow, skip weekends, assign one session per time slot per day
            var startDate = DateOnly.FromDateTime(DateTime.UtcNow.AddDays(7));
            var dateCursor = startDate;
            var periodSlots = timeSlots
                .Where(x => x.PeriodNumber <= timeSlots.Count - 1)
                .Select(x => x.PeriodNumber)
                .ToList();

            foreach (var task in tasks)
            {
                stoppingToken.ThrowIfCancellationRequested();

                // Skip if session already exists for this task
                if (existingTaskIds.Contains(task.Id))
                {
                    job.ProcessedCourses++;
                    continue;
                }

                // Query eligible students
                var eligible = await eligibilityService.GetEligibleStudentsAsync(
                    task.Id, stoppingToken);

                if (eligible.Count == 0)
                {
                    job.ProcessedCourses++;
                    continue;
                }

                // Skip weekends for date assignment
                while (dateCursor.DayOfWeek == DayOfWeek.Saturday ||
                       dateCursor.DayOfWeek == DayOfWeek.Sunday)
                    dateCursor = dateCursor.AddDays(1);

                // Determine time slot (rotate through available period slots)
                var periodIdx = (job.CreatedSessions) % Math.Max(periodSlots.Count, 1);
                var startPeriod = periodSlots[periodIdx];
                var periodCount = 2;

                // Resolve time
                var startSlot = slotLookup.GetValueOrDefault(startPeriod);
                var endSlot = slotLookup.GetValueOrDefault(startPeriod + periodCount - 1);
                if (startSlot is null || endSlot is null)
                {
                    job.ProcessedCourses++;
                    continue;
                }

                var startsAt = dateCursor.ToDateTime(startSlot.StartsAt, DateTimeKind.Utc);
                var endsAt = dateCursor.ToDateTime(endSlot.EndsAt, DateTimeKind.Utc);

                // Find a classroom (mixed-subject: no room conflict check)
                var room = allRooms.FirstOrDefault(r => r.Capacity >= eligible.Count);

                // Create session
                var session = new MakeupExamSession
                {
                    MakeupExamPlanId = plan.Id,
                    TeachingTaskId = task.Id,
                    ClassroomId = room?.Id,
                    ExamDate = dateCursor,
                    StartPeriod = startPeriod,
                    PeriodCount = periodCount,
                    StartsAt = startsAt,
                    EndsAt = endsAt,
                    RequiredInvigilatorCount = 2,
                    Enrollments = eligible.Select(e => new MakeupExamEnrollment
                    {
                        StudentId = e.StudentId,
                        Reason = e.Reason,
                        SourceGradeRecordId = e.SourceGradeRecordId,
                        SourceDeferredExamId = e.SourceDeferredExamId
                    }).ToList()
                };

                // Auto-assign invigilators
                var excludeTeacherIds = new HashSet<Guid>();
                var assigned = 0;
                foreach (var teacher in allTeachers)
                {
                    if (assigned >= 2) break;
                    if (excludeTeacherIds.Contains(teacher.Id)) continue;
                    session.Invigilators.Add(new MakeupExamSessionInvigilator
                    {
                        MakeupExamSessionId = session.Id,
                        TeacherId = teacher.Id
                    });
                    excludeTeacherIds.Add(teacher.Id);
                    assigned++;
                }

                db.MakeupExamSessions.Add(session);
                existingTaskIds.Add(task.Id);

                job.CreatedSessions++;
                job.EnrolledStudents += eligible.Count;
                job.ProcessedCourses++;

                messages.Add(
                    $"\"{task.CourseName}\"→{dateCursor:MM/dd} 第{startPeriod}节 " +
                    $"{(room != null ? room.Name : "无可用教室")} · {eligible.Count}人");

                // Advance date cursor every few sessions
                if (job.CreatedSessions % periodSlots.Count == 0)
                    dateCursor = dateCursor.AddDays(1);

                // Persist progress periodically
                if (job.ProcessedCourses % 10 == 0 || job.ProcessedCourses >= job.TotalCourses)
                    await PersistProgressAsync(job, stoppingToken);
            }

            job.Status = MakeupExamAutoJobStatus.Succeeded;
            job.MessagesJson = JsonSerializer.Serialize(messages.Take(20).ToList());
            job.CompletedAt = DateTime.UtcNow;
            await db.SaveChangesAsync(stoppingToken);

            logger.LogInformation(
                "Makeup exam auto job {JobId} completed: {Sessions} sessions, {Students} students.",
                job.Id, job.CreatedSessions, job.EnrolledStudents);
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            logger.LogInformation(
                "Makeup exam auto job {JobId} was interrupted by application shutdown.", jobId);
            throw;
        }
        catch (Exception exception)
        {
            logger.LogError(exception, "Makeup exam auto job {JobId} failed.", jobId);
            await MarkFailedAsync(jobId, exception);
        }
    }

    private async Task PersistProgressAsync(
        MakeupExamAutoJob job,
        CancellationToken cancellationToken)
    {
        try
        {
            await db.SaveChangesAsync(cancellationToken);
        }
        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
    }

    private async Task MarkFailedAsync(Guid jobId, Exception exception)
    {
        db.ChangeTracker.Clear();
        var job = await db.MakeupExamAutoJobs.FirstOrDefaultAsync(
            x => x.Id == jobId, CancellationToken.None);
        if (job is null) return;

        var message = exception.GetBaseException().Message;
        job.Status = MakeupExamAutoJobStatus.Failed;
        job.ErrorMessage = message.Length <= 2000 ? message : message[..2000];
        job.CompletedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(CancellationToken.None);
    }
}
