using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Graduation;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Controllers;

[ApiController]
[Authorize]
[Route("api/graduation-audits")]
public sealed class GraduationAuditsController(
    AppDbContext db,
    ICurrentUserDataScope currentUserDataScope) : ControllerBase
{
    private const string Managers =
        SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin;
    private const string Reviewers =
        Managers + "," + SystemRoles.CollegeAdmin;

    [HttpGet("batches")]
    [Authorize(Roles = Reviewers)]
    public async Task<ActionResult> GetBatches(CancellationToken token)
    {
        var collegeId = RestrictedCollegeId();
        return Ok(await db.GraduationAuditBatches.AsNoTracking()
            .OrderByDescending(x => x.GraduationYear)
            .ThenByDescending(x => x.CreatedAt)
            .Select(x => new
            {
                x.Id,
                x.Name,
                x.GraduationYear,
                x.EnrollmentYear,
                x.Status,
                x.Notes,
                x.CalculatedAt,
                x.PublishedAt,
                x.CreatedAt,
                ResultCount = x.Results.Count(result =>
                    !collegeId.HasValue ||
                    result.Student!.AdministrativeClass!.Major!.CollegeId == collegeId),
                EligibleCount = x.Results.Count(result =>
                    (!collegeId.HasValue ||
                     result.Student!.AdministrativeClass!.Major!.CollegeId == collegeId) &&
                    result.Conclusion == GraduationAuditConclusion.Eligible),
                IneligibleCount = x.Results.Count(result =>
                    (!collegeId.HasValue ||
                     result.Student!.AdministrativeClass!.Major!.CollegeId == collegeId) &&
                    result.Conclusion == GraduationAuditConclusion.Ineligible),
                OverrideCount = x.Results.Count(result =>
                    (!collegeId.HasValue ||
                     result.Student!.AdministrativeClass!.Major!.CollegeId == collegeId) &&
                    result.IsOverridden)
            }).ToListAsync(token));
    }

    [HttpGet("batches/{id:guid}")]
    [Authorize(Roles = Reviewers)]
    public async Task<ActionResult> GetBatch(
        Guid id,
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        [FromQuery] string? keyword = null,
        [FromQuery] GraduationAuditConclusion? conclusion = null,
        CancellationToken token = default)
    {
        page = Math.Max(page, 1);
        pageSize = Math.Clamp(pageSize, 10, 100);
        var batch = await db.GraduationAuditBatches.AsNoTracking()
            .FirstOrDefaultAsync(x => x.Id == id, token);
        if (batch is null) return NotFound();

        var collegeId = RestrictedCollegeId();
        var results = db.GraduationAuditResults.AsNoTracking()
            .Where(x => x.GraduationAuditBatchId == id);
        if (collegeId.HasValue)
            results = results.Where(x =>
                x.Student!.AdministrativeClass!.Major!.CollegeId == collegeId);

        var summary = await results
            .GroupBy(_ => 1)
            .Select(group => new
            {
                ResultCount = group.Count(),
                EligibleCount = group.Count(x =>
                    x.Conclusion == GraduationAuditConclusion.Eligible),
                IneligibleCount = group.Count(x =>
                    x.Conclusion == GraduationAuditConclusion.Ineligible),
                OverrideCount = group.Count(x => x.IsOverridden)
            })
            .FirstOrDefaultAsync(token);

        var normalizedKeyword = keyword?.Trim();
        if (!string.IsNullOrWhiteSpace(normalizedKeyword))
            results = results.Where(x =>
                x.Student!.StudentNumber.Contains(normalizedKeyword) ||
                x.Student.Name.Contains(normalizedKeyword) ||
                x.Student.AdministrativeClass!.Name.Contains(normalizedKeyword) ||
                x.Student.AdministrativeClass.Major!.Name.Contains(normalizedKeyword));
        if (conclusion.HasValue)
            results = results.Where(x => x.Conclusion == conclusion.Value);

        var total = await results.CountAsync(token);
        var items = await results
            .OrderBy(x => x.Student!.StudentNumber)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(x => new
            {
                x.Id,
                x.StudentId,
                x.Student!.StudentNumber,
                x.Student.Name,
                ClassName = x.Student.AdministrativeClass!.Name,
                MajorName = x.Student.AdministrativeClass.Major!.Name,
                CollegeName = x.Student.AdministrativeClass.Major.College!.Name,
                x.StudentStatusSnapshot,
                PlanName = x.CurriculumPlan != null ? x.CurriculumPlan.Name : null,
                x.RequiredCredits,
                x.EarnedCredits,
                x.RequiredCourseCount,
                x.PassedRequiredCourseCount,
                x.FailedCourseCount,
                x.MissingCourseNames,
                x.CalculatedConclusion,
                x.Conclusion,
                x.IsOverridden,
                x.ReviewComment,
                x.ReviewedAt
            }).ToListAsync(token);

        return Ok(new
        {
            batch.Id,
            batch.Name,
            batch.GraduationYear,
            batch.EnrollmentYear,
            batch.Status,
            batch.Notes,
            batch.CalculatedAt,
            batch.PublishedAt,
            ResultCount = summary?.ResultCount ?? 0,
            EligibleCount = summary?.EligibleCount ?? 0,
            IneligibleCount = summary?.IneligibleCount ?? 0,
            OverrideCount = summary?.OverrideCount ?? 0,
            Results = new
            {
                Items = items,
                Total = total,
                Page = page,
                PageSize = pageSize
            }
        });
    }

    [HttpPost("batches")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> CreateBatch(
        GraduationAuditBatchRequest request,
        CancellationToken token)
    {
        if (request.EnrollmentYear > request.GraduationYear)
            return ValidationProblem("入学年级不能晚于毕业年份。");
        var batch = new GraduationAuditBatch
        {
            Name = request.Name.Trim(),
            GraduationYear = request.GraduationYear,
            EnrollmentYear = request.EnrollmentYear,
            Notes = request.Notes?.Trim()
        };
        db.GraduationAuditBatches.Add(batch);
        await db.SaveChangesAsync(token);
        return Created(string.Empty, new { batch.Id });
    }

    [HttpPost("batches/{id:guid}/calculate")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> Calculate(Guid id, CancellationToken token)
    {
        var batch = await db.GraduationAuditBatches
            .FirstOrDefaultAsync(x => x.Id == id, token);
        if (batch is null) return NotFound();
        if (batch.Status != GraduationAuditBatchStatus.Draft)
            return ConflictProblem("已发布批次不能重新计算。");

        var students = await db.Students.AsNoTracking()
            .Include(x => x.AdministrativeClass)
            .ThenInclude(x => x!.Major)
            .Where(x => x.EnrollmentYear == batch.EnrollmentYear &&
                        (x.Status == StudentStatus.Active ||
                         x.Status == StudentStatus.Suspended))
            .OrderBy(x => x.StudentNumber)
            .ToListAsync(token);
        var plans = await db.CurriculumPlans.AsNoTracking()
            .Include(x => x.Modules)
            .ThenInclude(x => x.Courses)
            .ThenInclude(x => x.Course)
            .Where(x => x.EffectiveGrade == batch.EnrollmentYear &&
                        x.Status == CurriculumPlanStatus.Published)
            .ToListAsync(token);
        var grades = await db.GradeRecords.AsNoTracking()
            .Where(x => x.GradeSheet!.Status == GradeSheetStatus.Published)
            .Where(x => x.Student!.EnrollmentYear == batch.EnrollmentYear &&
                        (x.Student.Status == StudentStatus.Active ||
                         x.Student.Status == StudentStatus.Suspended))
            .Select(x => new GradeSnapshot(
                x.StudentId,
                x.GradeSheet!.TeachingTask!.CourseId,
                x.GradeSheet.TeachingTask.Course!.Name,
                x.GradeSheet.TeachingTask.Course.Credits,
                x.TotalScore,
                x.ExamStatus))
            .ToListAsync(token);

        var plansByMajor = plans
            .GroupBy(x => x.MajorId)
            .ToDictionary(group => group.Key, group => group.First());
        var gradesByStudent = grades
            .GroupBy(x => x.StudentId)
            .ToDictionary(group => group.Key, group => group.ToList());

        await db.GraduationAuditResults
            .Where(x => x.GraduationAuditBatchId == id)
            .ExecuteDeleteAsync(token);

        foreach (var student in students)
        {
            plansByMajor.TryGetValue(
                student.AdministrativeClass!.MajorId,
                out var plan);
            var studentGrades = gradesByStudent.GetValueOrDefault(student.Id) ?? [];
            var passedCourseIds = studentGrades
                .Where(IsPassed)
                .Select(x => x.CourseId)
                .Distinct()
                .ToHashSet();
            var earnedCredits = studentGrades
                .Where(IsPassed)
                .GroupBy(x => x.CourseId)
                .Sum(x => x.Max(item => item.Credits));
            var completion = CurriculumCompletionRules.Evaluate(
                plan?.Modules ?? [],
                passedCourseIds);
            var missingCourseNames = plan is null
                ? "未匹配已发布的培养方案"
                : string.Join("、", completion.MissingRequirements);
            if (missingCourseNames.Length > 2000)
                missingCourseNames = missingCourseNames[..2000];
            var failedCourseCount = studentGrades
                .GroupBy(x => x.CourseId)
                .Count(x => !x.Any(IsPassed));
            var conclusion = GraduationAuditRules.Evaluate(
                plan is not null,
                student.Status,
                plan?.TotalCredits ?? 0,
                earnedCredits,
                completion.RequirementCount,
                completion.PassedRequirementCount,
                failedCourseCount);
            db.GraduationAuditResults.Add(new GraduationAuditResult
            {
                GraduationAuditBatchId = batch.Id,
                StudentId = student.Id,
                CurriculumPlanId = plan?.Id,
                StudentStatusSnapshot = student.Status,
                RequiredCredits = plan?.TotalCredits ?? 0,
                EarnedCredits = earnedCredits,
                RequiredCourseCount = completion.RequirementCount,
                PassedRequiredCourseCount = completion.PassedRequirementCount,
                FailedCourseCount = failedCourseCount,
                MissingCourseNames = missingCourseNames,
                CalculatedConclusion = conclusion,
                Conclusion = conclusion
            });
        }
        batch.CalculatedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(token);
        return Ok(new { ResultCount = students.Count });
    }

    [HttpPut("results/{id:guid}")]
    [Authorize(Roles = Reviewers)]
    public async Task<ActionResult> ReviewResult(
        Guid id,
        GraduationAuditDecisionRequest request,
        CancellationToken token)
    {
        var result = await db.GraduationAuditResults
            .Include(x => x.GraduationAuditBatch)
            .Include(x => x.Student)
            .ThenInclude(x => x!.AdministrativeClass)
            .ThenInclude(x => x!.Major)
            .FirstOrDefaultAsync(x => x.Id == id, token);
        if (result is null) return NotFound();
        if (result.GraduationAuditBatch!.Status != GraduationAuditBatchStatus.Draft)
            return ConflictProblem("已发布的审核结果不能修改。");
        var collegeId = RestrictedCollegeId();
        if (collegeId.HasValue &&
            result.Student!.AdministrativeClass!.Major!.CollegeId != collegeId)
            return Forbid();

        result.Conclusion = request.Conclusion;
        result.IsOverridden = request.Conclusion != result.CalculatedConclusion;
        result.ReviewComment = request.Comment.Trim();
        result.ReviewedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(token);
        return NoContent();
    }

    [HttpPost("batches/{id:guid}/publish")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> Publish(Guid id, CancellationToken token)
    {
        var batch = await db.GraduationAuditBatches
            .Include(x => x.Results)
            .ThenInclude(x => x.Student)
            .FirstOrDefaultAsync(x => x.Id == id, token);
        if (batch is null) return NotFound();
        if (batch.Status != GraduationAuditBatchStatus.Draft)
            return ConflictProblem("该批次已经发布。");
        if (batch.Results.Count == 0)
            return ConflictProblem("请先计算毕业资格，再发布结果。");

        batch.Status = GraduationAuditBatchStatus.Published;
        batch.PublishedAt = DateTime.UtcNow;
        foreach (var result in batch.Results.Where(x =>
                     x.Conclusion == GraduationAuditConclusion.Eligible &&
                     x.Student!.Status == StudentStatus.Active))
            result.Student!.Status = StudentStatus.Graduated;
        await db.SaveChangesAsync(token);
        return NoContent();
    }

    [HttpGet("my-result")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> GetMyResult(CancellationToken token)
    {
        var userId = currentUserDataScope.Current.UserId;
        var result = await db.GraduationAuditResults.AsNoTracking()
            .Where(x => x.Student!.UserId == userId &&
                        x.GraduationAuditBatch!.Status ==
                            GraduationAuditBatchStatus.Published)
            .OrderByDescending(x => x.GraduationAuditBatch!.GraduationYear)
            .ThenByDescending(x => x.GraduationAuditBatch!.PublishedAt)
            .Select(x => new
            {
                x.Id,
                BatchName = x.GraduationAuditBatch!.Name,
                x.GraduationAuditBatch.GraduationYear,
                x.Student!.StudentNumber,
                x.Student.Name,
                MajorName = x.Student.AdministrativeClass!.Major!.Name,
                PlanName = x.CurriculumPlan != null ? x.CurriculumPlan.Name : null,
                x.RequiredCredits,
                x.EarnedCredits,
                x.RequiredCourseCount,
                x.PassedRequiredCourseCount,
                x.FailedCourseCount,
                x.MissingCourseNames,
                x.Conclusion,
                x.IsOverridden,
                x.ReviewComment,
                x.GraduationAuditBatch.PublishedAt
            }).FirstOrDefaultAsync(token);
        return Ok(result);
    }

    private Guid? RestrictedCollegeId()
    {
        var scope = currentUserDataScope.Current;
        return scope.IsInRole(SystemRoles.CollegeAdmin)
            ? scope.CollegeId ?? Guid.Empty
            : null;
    }

    private static bool IsPassed(GradeSnapshot grade) =>
        grade.ExamStatus == GradeExamStatus.Exempt ||
        grade.TotalScore is decimal score && score >= 60;

    private sealed record GradeSnapshot(
        Guid StudentId,
        Guid CourseId,
        string CourseName,
        decimal Credits,
        decimal? TotalScore,
        GradeExamStatus ExamStatus);

    private ActionResult ConflictProblem(string detail) => Conflict(new ProblemDetails
    {
        Title = "无法完成毕业审核操作",
        Detail = detail,
        Status = StatusCodes.Status409Conflict
    });
}

public sealed record GraduationAuditBatchRequest(
    [Required, MinLength(3), MaxLength(120)] string Name,
    [Range(2000, 2200)] int GraduationYear,
    [Range(2000, 2200)] int EnrollmentYear,
    [MaxLength(500)] string? Notes);

public sealed record GraduationAuditDecisionRequest(
    GraduationAuditConclusion Conclusion,
    [Required, MinLength(5), MaxLength(500)] string Comment);
