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

namespace Jiaowu.Api.Infrastructure.Exams;

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

            job.Status = ExamPublishJobStatus.Running;
            job.StartedAt ??= DateTime.UtcNow;
            job.CompletedAt = null;
            job.ErrorMessage = null;
            job.CurrentStep = "正在校验考试计划";
            await db.SaveChangesAsync(stoppingToken);

            switch (job.Kind)
            {
                case ExamPublishJobKind.FormalExam:
                    await PublishFormalExamAsync(job, stoppingToken);
                    break;
                case ExamPublishJobKind.MakeupExam:
                    await PublishMakeupExamAsync(job, stoppingToken);
                    break;
                case ExamPublishJobKind.ExperimentProjects:
                    await PublishExperimentProjectsAsync(job, stoppingToken);
                    break;
                default:
                    throw new InvalidOperationException(
                        $"不支持的考试发布类型：{job.Kind}。");
            }

            job.Status = ExamPublishJobStatus.Succeeded;
            job.ActivePlanId = null;
            job.CurrentStep = "发布完成";
            job.CompletedAt = DateTime.UtcNow;
            await db.SaveChangesAsync(stoppingToken);

            await cache.RemoveByTagAsync(
                AppCacheTags.Timetables,
                stoppingToken);

            logger.LogInformation(
                "Exam publish job {JobId} for {Kind}/{PlanId} completed.",
                job.Id,
                job.Kind,
                job.PlanId);
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            logger.LogInformation(
                "Exam publish job {JobId} was interrupted by application shutdown.",
                jobId);
            throw;
        }
        catch (ExamPublishValidationException validationException)
        {
            logger.LogWarning(
                validationException,
                "Exam publish job {JobId} validation failed.",
                jobId);
            await MarkFailedAsync(jobId, validationException.Message);
        }
        catch (Exception exception)
        {
            logger.LogError(exception, "Exam publish job {JobId} failed.", jobId);
            await MarkFailedAsync(jobId, exception.GetBaseException().Message);
        }
    }

    private async Task PublishFormalExamAsync(
        ExamPublishJob job,
        CancellationToken ct)
    {
        var plan = await db.ExamPlans
            .FirstOrDefaultAsync(x => x.Id == job.PlanId, ct);
        if (plan is null)
            throw new ExamPublishValidationException("考试计划不存在。");
        if (plan.Status != ExamPlanStatus.Draft)
            throw new ExamPublishValidationException("只有草稿考试计划可以发布。");

        // Step 1: check sessions exist — simple count, no JOIN
        var sessionCount = await db.ExamSessions
            .CountAsync(x => x.ExamPlanId == job.PlanId, ct);
        if (sessionCount == 0)
            throw new ExamPublishValidationException(
                "至少安排一个考试场次后才能发布。");

        // Step 2: load session summaries — only necessary columns, no Include chains
        var sessions = await db.ExamSessions
            .AsNoTracking()
            .Where(x => x.ExamPlanId == job.PlanId)
            .Select(x => new
            {
                x.Id,
                x.TeachingTaskId,
                x.ClassroomId,
                InvigilatorCount = x.Invigilators.Count,
                RoomLinkCount = x.RoomLinks.Count
            })
            .ToListAsync(ct);

        // Step 3: load roster counts — independent query
        var taskIds = sessions.Select(x => x.TeachingTaskId).ToArray();
        var rosterCounts = (await TeachingTaskRosterQuery
                .LoadForTasksAsync(db, taskIds, ct))
            .GroupBy(x => x.TeachingTaskId)
            .ToDictionary(x => x.Key, x => x.Count());

        // Step 4: validate each session's assignment completeness
        var unassignedSessionIds = new List<Guid>();
        foreach (var session in sessions)
        {
            var rosterCount = rosterCounts.GetValueOrDefault(session.TeachingTaskId);
            if (session.RoomLinkCount > 0)
            {
                // Has room links — check seat assignment via separate query
                var assignedSeatCount = await db.ExamSeats
                    .CountAsync(seat =>
                            seat.ExamSessionId == session.Id,
                        ct);
                if (assignedSeatCount != rosterCount)
                    unassignedSessionIds.Add(session.Id);
            }
            else if (!session.ClassroomId.HasValue ||
                     session.InvigilatorCount == 0)
            {
                unassignedSessionIds.Add(session.Id);
            }
        }

        if (unassignedSessionIds.Count > 0)
            throw new ExamPublishValidationException(
                $"还有 {unassignedSessionIds.Count} 个教学班未完成考场座位或监考安排，" +
                "请先完成自动编排。");

        // Step 5: check room invigilator sufficiency — separate query
        var insufficientInvigilatorCount = await db.ExamRoomSessions
            .Where(link =>
                link.ExamRoom!.ExamPlanId == job.PlanId &&
                link.ExamRoom.Invigilators.Count <
                link.ExamRoom.RequiredInvigilatorCount)
            .Select(link => link.ExamSessionId)
            .Distinct()
            .CountAsync(ct);
        if (insufficientInvigilatorCount > 0)
            throw new ExamPublishValidationException(
                $"还有 {insufficientInvigilatorCount} 个场次的混排考场监考教师不足，" +
                "请先完成自动编排。");

        // Step 6: validate mixed rooms — split into two independent queries
        //   to avoid Seats × SessionLinks Cartesian product

        // 6a: capacity overflow
        var overCapacityCount = await db.ExamRooms
            .Where(r => r.ExamPlanId == job.PlanId)
            .Select(r => new
            {
                r.Id,
                SeatCount = r.Seats.Count,
                Capacity = r.Classroom!.Capacity
            })
            .CountAsync(x => x.SeatCount > x.Capacity, ct);
        if (overCapacityCount > 0)
            throw new ExamPublishValidationException(
                $"发现 {overCapacityCount} 个考场容量超限，请重新编排。");

        // 6b: course mismatch
        var courseMismatchCount = await db.ExamRoomSessions
            .Where(link =>
                link.ExamRoom!.ExamPlanId == job.PlanId &&
                link.ExamSession!.TeachingTask!.CourseId !=
                link.ExamRoom.CourseId)
            .Select(link => link.ExamRoomId)
            .Distinct()
            .CountAsync(ct);
        if (courseMismatchCount > 0)
            throw new ExamPublishValidationException(
                $"发现 {courseMismatchCount} 个考场混入不同课程，请重新编排。");

        // Step 7: publish
        plan.Status = ExamPlanStatus.Published;
        plan.PublishedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(ct);
    }

    private async Task PublishMakeupExamAsync(
        ExamPublishJob job,
        CancellationToken ct)
    {
        var plan = await db.MakeupExamPlans
            .FirstOrDefaultAsync(x => x.Id == job.PlanId, ct);
        if (plan is null)
            throw new ExamPublishValidationException("补考计划不存在。");
        if (plan.Status != MakeupExamPlanStatus.Draft)
            throw new ExamPublishValidationException("只有草稿补考计划可以发布。");

        // Step 1: check sessions exist — simple count
        var sessionCount = await db.MakeupExamSessions
            .CountAsync(x => x.MakeupExamPlanId == job.PlanId, ct);
        if (sessionCount == 0)
            throw new ExamPublishValidationException(
                "至少安排一个考试场次后才能发布。");

        // Step 2: load session summaries — only necessary columns
        var sessions = await db.MakeupExamSessions
            .AsNoTracking()
            .Where(x => x.MakeupExamPlanId == job.PlanId)
            .Select(x => new
            {
                x.Id,
                x.ClassroomId,
                InvigilatorCount = x.Invigilators.Count,
                EnrollmentCount = x.Enrollments.Count
            })
            .ToListAsync(ct);

        // Step 3: validate completeness — no Include chain needed
        var unassigned = sessions.Count(x =>
            !x.ClassroomId.HasValue || x.InvigilatorCount == 0);
        if (unassigned > 0)
            throw new ExamPublishValidationException(
                $"还有 {unassigned} 个场次未分配考场或监考教师，请先完成自动编排。");

        // Step 4: validate enrollments
        var empty = sessions.Count(x => x.EnrollmentCount == 0);
        if (empty > 0)
            throw new ExamPublishValidationException(
                $"还有 {empty} 个场次没有登记补考学生。");

        // Step 5: publish
        plan.Status = MakeupExamPlanStatus.Published;
        plan.PublishedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(ct);
    }

    private async Task PublishExperimentProjectsAsync(ExamPublishJob job, CancellationToken ct)
    {
        var ids = JsonSerializer.Deserialize<List<Guid>>(job.ProjectIdsJson ?? "[]")?
            .Where(x => x != Guid.Empty).Distinct().ToList() ?? [];
        if (ids.Count is 0 or > 100)
            throw new ExamPublishValidationException("实验发布任务的数据无效。请重新提交。");

        var projects = await db.ExperimentProjects
            .Include(x => x.Sessions)
            .ThenInclude(x => x.Instructors)
            .Include(x => x.TeachingTask)
            .ThenInclude(x => x!.Course)
            .Where(x => ids.Contains(x.Id))
            .ToListAsync(ct);
        if (projects.Count != ids.Count)
            throw new ExamPublishValidationException("部分实验项目不存在，请刷新后重新提交。");

        foreach (var project in projects)
        {
            if (project.Status != ExperimentProjectStatus.Draft)
                throw new ExamPublishValidationException("批量发布只能包含草稿实验项目。");
            var hasSchedule = project.ArrangementMode == ExperimentArrangementMode.Centralized
                ? project.ScheduleEntryId.HasValue || project.Sessions.Any(x => x.Status == ExperimentSessionStatus.Scheduled)
                : project.Sessions.Any(x => x.Status == ExperimentSessionStatus.Scheduled);
            if (!hasSchedule)
                throw new ExamPublishValidationException($"“{project.Name}”尚未具备发布条件。");
            if (project.Sessions.Any(x => x.Status == ExperimentSessionStatus.Scheduled &&
                (x.SessionDate < project.StartDate || x.SessionDate > project.EndDate)))
                throw new ExamPublishValidationException($"“{project.Name}”存在不在开放日期范围内的实验场次。");
            if (project.ArrangementMode == ExperimentArrangementMode.SelfScheduled &&
                (project.SelectionStartsAt is null || project.SelectionEndsAt is null ||
                 project.SelectionStartsAt >= project.SelectionEndsAt))
                throw new ExamPublishValidationException($"“{project.Name}”未设置有效的选课时间范围。");
            if (project.ArrangementMode == ExperimentArrangementMode.SelfScheduled &&
                project.Sessions.Any(x => x.Status == ExperimentSessionStatus.Scheduled &&
                                          x.Instructors.Count == 0))
                throw new ExamPublishValidationException($"“{project.Name}”存在未指定指导老师的实验场次。");
        }

        job.CurrentStep = "正在发布实验项目";
        await db.SaveChangesAsync(ct);
        var publishedAt = DateTime.UtcNow;
        foreach (var project in projects)
        {
            project.Status = ExperimentProjectStatus.Published;
            project.PublishedAt = publishedAt;
        }
        await db.SaveChangesAsync(ct);

        foreach (var project in projects)
        {
            var userIds = await TeachingTaskRosterQuery.ForTask(db, project.TeachingTaskId)
                .Where(x => x.UserId.HasValue).Select(x => x.UserId!.Value).Distinct().ToListAsync(ct);
            if (userIds.Count == 0) continue;
            var mode = project.ArrangementMode == ExperimentArrangementMode.Centralized ? "集中安排" : "自行预约";
            await NotificationService.SendToUserIdsAsync(db, userIds, "实验项目已发布",
                $"《{project.TeachingTask!.Course!.Name}》已发布“{project.Name}”（{mode}），请查看实验安排。",
                "/experiments", ct, NotificationCategory.Schedule);
        }
    }

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

        job.Status = ExamPublishJobStatus.Failed;
        job.ActivePlanId = null;
        job.CurrentStep = "发布失败";
        job.ErrorMessage = message.Length <= 2000 ? message : message[..2000];
        job.CompletedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(CancellationToken.None);
    }
}

public sealed class ExamPublishValidationException(string message)
    : InvalidOperationException(message);
