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 ExamSignInExportJobProcessor(
    AppDbContext db,
    ILogger<ExamSignInExportJobProcessor> logger)
{
    public async Task ProcessAsync(Guid jobId, CancellationToken stoppingToken)
    {
        try
        {
            var job = await db.ExamSignInExportJobs
                .FirstOrDefaultAsync(x => x.Id == jobId, stoppingToken);
            if (job is null ||
                job.Status is ExamSignInExportJobStatus.Succeeded
                    or ExamSignInExportJobStatus.Failed)
            {
                return;
            }

            job.Status = ExamSignInExportJobStatus.Running;
            job.StartedAt ??= DateTime.UtcNow;
            job.CompletedAt = null;
            job.ErrorMessage = null;
            job.CurrentStep = "正在生成考场签名单";
            await db.SaveChangesAsync(stoppingToken);

            var plan = await db.ExamPlans.AsNoTracking()
                .Where(x => x.Id == job.PlanId)
                .Select(x => new
                {
                    x.Name,
                    TermName = x.AcademicTerm!.Name
                })
                .FirstOrDefaultAsync(stoppingToken);
            if (plan is null)
            {
                await MarkFailedAsync(jobId, "考试计划不存在。");
                return;
            }

            var rooms = await db.ExamRooms.AsNoTracking()
                .Include(x => x.Course)
                .Include(x => x.Classroom)
                    .ThenInclude(x => x!.Building)
                .Include(x => x.Invigilators)
                    .ThenInclude(x => x.Teacher)
                .Include(x => x.SessionLinks)
                    .ThenInclude(x => x.ExamSession)
                        .ThenInclude(x => x!.TeachingTask)
                .Include(x => x.Seats)
                    .ThenInclude(x => x.Student)
                        .ThenInclude(x => x!.AdministrativeClass)
                .Where(x => x.ExamPlanId == job.PlanId)
                .OrderBy(x => x.ExamDate)
                .ThenBy(x => x.StartPeriod)
                .ThenBy(x => x.Classroom!.Building!.Name)
                .ThenBy(x => x.Classroom!.Name)
                .AsSplitQuery()
                .ToListAsync(stoppingToken);

            var legacySessions = await db.ExamSessions.AsNoTracking()
                .Where(x => x.ExamPlanId == job.PlanId)
                .Where(x => !x.RoomLinks.Any())
                .OrderBy(x => x.ExamDate)
                .ThenBy(x => x.StartPeriod)
                .Select(session => new
                {
                    session.Id,
                    session.TeachingTaskId,
                    session.ExamDate,
                    session.StartsAt,
                    session.EndsAt,
                    CourseCode = session.TeachingTask!.Course!.Code,
                    CourseName = session.TeachingTask.Course.Name,
                    session.TeachingTask.TaskNumber,
                    BuildingName = session.Classroom != null
                        ? session.Classroom.Building!.Name
                        : null,
                    ClassroomName = session.Classroom != null
                        ? session.Classroom.Name
                        : null,
                    InvigilatorNames = session.Invigilators
                        .OrderBy(item => item.Teacher!.TeacherNumber)
                        .Select(item => item.Teacher!.Name)
                })
                .ToListAsync(stoppingToken);

            if (rooms.Count == 0 && legacySessions.Count == 0)
            {
                await MarkFailedAsync(jobId, "当前考试计划没有可导出的考试场次。");
                return;
            }

            var roster = await TeachingTaskRosterQuery.LoadForTasksAsync(
                db,
                legacySessions.Select(x => x.TeachingTaskId),
                stoppingToken);
            var studentsByTask = roster.ToLookup(x => x.TeachingTaskId);

            var sheets = rooms.Select(room => new ExamSignInSessionData(
                    room.Id,
                    room.ExamDate,
                    room.StartsAt,
                    room.EndsAt,
                    room.Course!.Code,
                    room.Course.Name,
                    string.Join(
                        "、",
                        room.SessionLinks
                            .Select(link =>
                                link.ExamSession!.TeachingTask!.TaskNumber)
                            .Distinct()
                            .OrderBy(x => x)),
                    room.Classroom!.Building!.Name,
                    room.Classroom.Name,
                    room.Invigilators
                        .OrderBy(item => item.Teacher!.TeacherNumber)
                        .Select(item => item.Teacher!.Name)
                        .ToList(),
                    room.Seats
                        .OrderBy(seat => seat.SeatNumber)
                        .Select(seat => new ExamSignInStudentData(
                            seat.StudentId,
                            seat.Student!.StudentNumber,
                            seat.Student.Name,
                            seat.Student.AdministrativeClass!.Name,
                            seat.SeatNumber))
                        .ToList()))
                .Concat(legacySessions.Select(session =>
                    new ExamSignInSessionData(
                        session.Id,
                        session.ExamDate,
                        session.StartsAt,
                        session.EndsAt,
                        session.CourseCode,
                        session.CourseName,
                        session.TaskNumber,
                        session.BuildingName,
                        session.ClassroomName,
                        session.InvigilatorNames.ToList(),
                        studentsByTask[session.TeachingTaskId]
                            .Select((student, index) =>
                                new ExamSignInStudentData(
                                    student.StudentId,
                                    student.StudentNumber,
                                    student.Name,
                                    student.ClassName,
                                    index + 1))
                            .ToList())))
                .ToList();

            var data = new ExamSignInWorkbookData(plan.Name, plan.TermName, sheets);
            var bytes = ExamSignInWorkbookExporter.Create(data);

            job.Status = ExamSignInExportJobStatus.Succeeded;
            job.FileName = $"考场签名单-{SanitizeFileName(plan.Name)}.xlsx";
            job.FileBytes = bytes;
            job.FileSize = bytes.Length;
            job.CurrentStep = "生成完成";
            job.CompletedAt = DateTime.UtcNow;
            await db.SaveChangesAsync(stoppingToken);

            logger.LogInformation(
                "Sign-in export job {JobId} for plan {PlanId} completed, {Size:N0} bytes.",
                job.Id,
                job.PlanId,
                bytes.Length);
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            logger.LogInformation(
                "Sign-in export job {JobId} was interrupted by application shutdown.",
                jobId);
            throw;
        }
        catch (Exception exception)
        {
            logger.LogError(exception, "Sign-in export job {JobId} failed.", jobId);
            await MarkFailedAsync(jobId, exception.GetBaseException().Message);
        }
    }

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

        job.Status = ExamSignInExportJobStatus.Failed;
        job.CurrentStep = "生成失败";
        job.ErrorMessage = message.Length <= 2000 ? message : message[..2000];
        job.CompletedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(CancellationToken.None);
    }

    private static string SanitizeFileName(string value)
    {
        var invalid = System.IO.Path.GetInvalidFileNameChars().ToHashSet();
        var normalized = new string(value
            .Select(c => invalid.Contains(c) ? '_' : c)
            .ToArray()).Trim();
        return normalized.Length > 100 ? normalized[..100] : normalized;
    }
}
