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

namespace Jiaowu.Api.Controllers;

[ApiController]
[Authorize]
[Route("api/approvals")]
public sealed class ApprovalsController(AppDbContext db, ICurrentUserDataScope scope) : ControllerBase
{
    private const string Managers = SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin + "," + SystemRoles.CollegeAdmin;
    private const string Teachers = Managers + "," + SystemRoles.Teacher;
    private const string Students = Managers + "," + SystemRoles.Student;

    // ═══════════════ Aggregated pending ═══════════════
    [HttpGet("pending")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult<PagedResult<ApprovalItem>>> GetPending(
        int page = 1,
        int pageSize = 20,
        CancellationToken ct = default)
    {
        if (page < 1 || pageSize is < 1 or > 100)
            return ValidationProblem("page 必须大于 0，pageSize 必须在 1 到 100 之间。");

        var take = checked(page * pageSize);
        var total = 0;
        var items = new List<ApprovalItem>(take * 8);

        items.AddRange(await Scoped<CourseExemption>(x => x.Status == ApprovalStatus.Submitted)
            .OrderByDescending(x => x.SubmittedAt).Take(take)
            .Select(x => new ApprovalItem(x.Id, "CourseExemption", "免修", $"{x.Student!.Name} — 《{x.TeachingTask!.Course!.Name}》", x.Reason, x.SubmittedAt, x.TeachingTask.Course!.College!.Name))
            .ToListAsync(ct));
        total += await Scoped<CourseExemption>(x => x.Status == ApprovalStatus.Submitted).CountAsync(ct);

        items.AddRange(await Scoped<DeferredExam>(x => x.Status == ApprovalStatus.Submitted)
            .OrderByDescending(x => x.SubmittedAt).Take(take)
            .Select(x => new ApprovalItem(x.Id, "DeferredExam", "缓考", $"{x.Student!.Name} — 《{x.TeachingTask!.Course!.Name}》", x.Reason, x.SubmittedAt, x.TeachingTask.Course!.College!.Name))
            .ToListAsync(ct));
        total += await Scoped<DeferredExam>(x => x.Status == ApprovalStatus.Submitted).CountAsync(ct);

        items.AddRange(await Scoped<GradeModification>(x => x.Status == GradeModificationStatus.TeacherSubmitted || x.Status == GradeModificationStatus.CollegeApproved)
            .OrderByDescending(x => x.SubmittedAt).Take(take)
            .Select(x => new ApprovalItem(x.Id, "GradeModification", "成绩修改", $"{x.GradeRecord!.Student!.Name} — 《{x.GradeRecord.GradeSheet!.TeachingTask!.Course!.Name}》", $"{x.CurrentScore} → {x.RequestedScore}：{x.Reason}", x.SubmittedAt, x.GradeRecord.GradeSheet.TeachingTask.Course!.College!.Name))
            .ToListAsync(ct));
        total += await Scoped<GradeModification>(x => x.Status == GradeModificationStatus.TeacherSubmitted || x.Status == GradeModificationStatus.CollegeApproved).CountAsync(ct);

        items.AddRange(await Scoped<CourseSubstitution>(x => x.Status == ApprovalStatus.Submitted)
            .OrderByDescending(x => x.SubmittedAt).Take(take)
            .Select(x => new ApprovalItem(x.Id, "CourseSubstitution", "课程替代", $"{x.Student!.Name}：{x.SubstituteCourse!.Name} → {x.OriginalCourse!.Name}", x.Reason, x.SubmittedAt, x.Student.AdministrativeClass!.Major!.College!.Name))
            .ToListAsync(ct));
        total += await Scoped<CourseSubstitution>(x => x.Status == ApprovalStatus.Submitted).CountAsync(ct);

        items.AddRange(await Scoped<StudentStatusChange>(x => x.State != StudentStatusChangeState.Approved && x.State != StudentStatusChangeState.Rejected && x.State != StudentStatusChangeState.Cancelled)
            .OrderByDescending(x => x.SubmittedAt).Take(take)
            .Select(x => new ApprovalItem(x.Id, "StudentStatusChange", "学籍异动", $"{x.Student!.Name} — {SSCLabel(x.Type)}", x.Reason, x.SubmittedAt, x.Student.AdministrativeClass!.Major!.College!.Name))
            .ToListAsync(ct));
        total += await Scoped<StudentStatusChange>(x => x.State != StudentStatusChangeState.Approved && x.State != StudentStatusChangeState.Rejected && x.State != StudentStatusChangeState.Cancelled).CountAsync(ct);

        items.AddRange(await Scoped<CourseAdjustment>(x => x.Status == CourseAdjustmentStatus.Submitted)
            .OrderByDescending(x => x.SubmittedAt).Take(take)
            .Select(x => new ApprovalItem(x.Id, "CourseAdjustment", CALabel(x.Type), $"《{x.TeachingTask!.Course!.Name}》", x.Reason, x.SubmittedAt!.Value, x.TeachingTask.Course.College!.Name))
            .ToListAsync(ct));
        total += await Scoped<CourseAdjustment>(x => x.Status == CourseAdjustmentStatus.Submitted).CountAsync(ct);

        items.AddRange(await Scoped<GradeSheet>(x => x.Status == GradeSheetStatus.Submitted)
            .OrderByDescending(x => x.SubmittedAt).Take(take)
            .Select(x => new ApprovalItem(x.Id, "GradeSheet", "成绩审核", $"《{x.TeachingTask!.Course!.Name}》— {x.Records.Count}人", "教师已提交成绩", x.SubmittedAt!.Value, x.TeachingTask.Course.College!.Name))
            .ToListAsync(ct));
        total += await Scoped<GradeSheet>(x => x.Status == GradeSheetStatus.Submitted).CountAsync(ct);

        items.AddRange(await Scoped<AttendanceRecord>(x => x.AppealStatus == AttendanceAppealStatus.Pending)
            .OrderByDescending(x => x.AppealSubmittedAt).Take(take)
            .Select(x => new ApprovalItem(x.StudentId, "AttendanceAppeal", "考勤申诉", $"{x.Student!.Name} — 《{x.AttendanceSheet!.TeachingTask!.Course!.Name}》", x.AppealReason ?? "", x.AppealSubmittedAt!.Value, x.Student.AdministrativeClass!.Major!.College!.Name))
            .ToListAsync(ct));
        total += await Scoped<AttendanceRecord>(x => x.AppealStatus == AttendanceAppealStatus.Pending).CountAsync(ct);

        var pageItems = items
            .OrderByDescending(x => x.Time)
            .ThenByDescending(x => x.Id)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .ToArray();
        return Ok(new PagedResult<ApprovalItem>(pageItems, total, page, pageSize));
    }

    // ═══════════════ Course Exemption ═══════════════
    [HttpGet("exemptions/mine")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> GetMyExemptions(CancellationToken ct)
    {
        var studentId = await GetStudentIdAsync(ct);
        if (studentId is null) return StudentNotFound();
        return Ok(await db.CourseExemptions.AsNoTracking()
            .Where(x => x.StudentId == studentId).OrderByDescending(x => x.CreatedAt)
            .Select(x => new { x.Id, x.Status, x.Reason, x.ReviewComment, x.SubmittedAt, CourseName = x.TeachingTask!.Course!.Name })
            .ToListAsync(ct));
    }

    [HttpPost("exemptions")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> ApplyExemption(ExemptionRequest req, CancellationToken ct)
    {
        var studentId = await GetStudentIdAsync(ct);
        if (studentId is null) return StudentNotFound();
        if (!await IsStudentCourseAvailableForApplicationAsync(
                studentId.Value, req.TeachingTaskId, ct))
            return ConflictProblem("该课程不在您当前或已发布课表的修读课程中，请刷新课程列表后重试。");
        if (await db.CourseExemptions.AnyAsync(x => x.StudentId == studentId && x.TeachingTaskId == req.TeachingTaskId && x.Status == ApprovalStatus.Submitted, ct))
            return ConflictProblem("该课程已有免修申请在审核中。");
        var ex = new CourseExemption { StudentId = studentId.Value, TeachingTaskId = req.TeachingTaskId, Reason = req.Reason.Trim() };
        db.CourseExemptions.Add(ex);
        await db.SaveChangesAsync(ct);
        // Reload to get nav properties for notification
        var info = await db.CourseExemptions.Where(x => x.Id == ex.Id)
            .Select(x => new { StudentName = x.Student!.Name, CourseName = x.TeachingTask!.Course!.Name })
            .FirstAsync(ct);
        await NotifyManagers("免修申请", $"{info.StudentName} 申请免修《{info.CourseName}》", ct);
        return Created("", new { ex.Id });
    }

    [HttpPost("exemptions/{id:guid}/approve")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> ApproveExemption(Guid id, CancellationToken ct)
    {
        var ex = await db.CourseExemptions.Include(x => x.TeachingTask).ThenInclude(x => x!.Course).FirstOrDefaultAsync(x => x.Id == id, ct);
        if (ex is null) return NotFound();
        ex.Status = ApprovalStatus.Approved; ex.ReviewedAt = DateTime.UtcNow; ex.ReviewedByUserId = scope.Current.UserId;
        // Auto-apply: mark grade record as exempt
        var sheet = await db.GradeSheets.FirstOrDefaultAsync(x => x.TeachingTaskId == ex.TeachingTaskId, ct);
        if (sheet is not null)
        {
            var record = await db.GradeRecords.FirstOrDefaultAsync(x => x.GradeSheetId == sheet.Id && x.StudentId == ex.StudentId, ct);
            if (record is not null) { record.ExamStatus = GradeExamStatus.Exempt; record.RegularScore = null; record.FinalScore = null; record.TotalScore = null; record.GradePoint = null; }
        }
        await db.SaveChangesAsync(ct);
        await NotifyStudent(ex.StudentId, "免修申请已通过", $"《{ex.TeachingTask!.Course!.Name}》免修已批准。", ct);
        return NoContent();
    }

    [HttpPost("exemptions/{id:guid}/reject")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> RejectExemption(Guid id, [FromBody] ReviewBody body, CancellationToken ct)
    {
        var ex = await db.CourseExemptions.FindAsync([id], ct);
        if (ex is null) return NotFound();
        ex.Status = ApprovalStatus.Rejected; ex.ReviewedAt = DateTime.UtcNow; ex.ReviewComment = body.Comment?.Trim(); ex.ReviewedByUserId = scope.Current.UserId;
        await db.SaveChangesAsync(ct);
        await NotifyStudent(ex.StudentId, "免修申请已驳回", ex.ReviewComment ?? "审核未通过。", ct);
        return NoContent();
    }

    // ═══════════════ Deferred Exam ═══════════════
    [HttpGet("deferred/mine")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> GetMyDeferred(CancellationToken ct)
    {
        var sid = await GetStudentIdAsync(ct);
        if (sid is null) return StudentNotFound();
        return Ok(await db.DeferredExams.AsNoTracking().Where(x => x.StudentId == sid).OrderByDescending(x => x.CreatedAt)
            .Select(x => new { x.Id, x.Status, x.Reason, x.ReviewComment, x.SubmittedAt, CourseName = x.TeachingTask!.Course!.Name }).ToListAsync(ct));
    }

    [HttpPost("deferred")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> ApplyDeferred(ExemptionRequest req, CancellationToken ct)
    {
        var sid = await GetStudentIdAsync(ct);
        if (sid is null) return StudentNotFound();
        if (!await IsStudentCourseAvailableForApplicationAsync(
                sid.Value, req.TeachingTaskId, ct))
            return ConflictProblem("该课程不在您当前或已发布课表的修读课程中，请刷新课程列表后重试。");
        if (await db.DeferredExams.AnyAsync(x => x.StudentId == sid && x.TeachingTaskId == req.TeachingTaskId && x.Status == ApprovalStatus.Submitted, ct))
            return ConflictProblem("该课程已有缓考申请在审核中。");
        var d = new DeferredExam { StudentId = sid.Value, TeachingTaskId = req.TeachingTaskId, Reason = req.Reason.Trim() };
        db.DeferredExams.Add(d);
        await db.SaveChangesAsync(ct);
        var info = await db.DeferredExams.Where(x => x.Id == d.Id)
            .Select(x => new { StudentName = x.Student!.Name, CourseName = x.TeachingTask!.Course!.Name })
            .FirstAsync(ct);
        await NotifyManagers("缓考申请", $"{info.StudentName} 申请《{info.CourseName}》缓考", ct);
        return Created("", new { d.Id });
    }

    [HttpPost("deferred/{id:guid}/approve")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> ApproveDeferred(Guid id, CancellationToken ct)
    {
        var d = await db.DeferredExams.Include(x => x.TeachingTask).ThenInclude(x => x!.Course).FirstOrDefaultAsync(x => x.Id == id, ct);
        if (d is null) return NotFound();
        d.Status = ApprovalStatus.Approved; d.ReviewedAt = DateTime.UtcNow; d.ReviewedByUserId = scope.Current.UserId;
        // Auto-apply: set grade record to deferred
        var sheet = await db.GradeSheets.FirstOrDefaultAsync(x => x.TeachingTaskId == d.TeachingTaskId, ct);
        if (sheet is not null)
        {
            var record = await db.GradeRecords.FirstOrDefaultAsync(x => x.GradeSheetId == sheet.Id && x.StudentId == d.StudentId, ct);
            if (record is not null) record.ExamStatus = GradeExamStatus.Deferred;
        }
        await db.SaveChangesAsync(ct);
        await NotifyStudent(d.StudentId, "缓考申请已通过", $"《{d.TeachingTask!.Course!.Name}》缓考已批准。", ct);
        return NoContent();
    }

    [HttpPost("deferred/{id:guid}/reject")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> RejectDeferred(Guid id, [FromBody] ReviewBody body, CancellationToken ct)
    {
        var d = await db.DeferredExams.FindAsync([id], ct);
        if (d is null) return NotFound();
        d.Status = ApprovalStatus.Rejected; d.ReviewedAt = DateTime.UtcNow; d.ReviewComment = body.Comment?.Trim(); d.ReviewedByUserId = scope.Current.UserId;
        await db.SaveChangesAsync(ct);
        await NotifyStudent(d.StudentId, "缓考申请已驳回", d.ReviewComment ?? "审核未通过。", ct);
        return NoContent();
    }

    // ═══════════════ Grade Modification (3-level) ═══════════════
    [HttpPost("grade-modifications")]
    [Authorize(Roles = Teachers)]
    public async Task<ActionResult> ApplyGradeMod(GradeModRequest req, CancellationToken ct)
    {
        var record = await db.GradeRecords.Include(x => x.GradeSheet).FirstOrDefaultAsync(x => x.Id == req.GradeRecordId, ct);
        if (record is null) return NotFound();
        if (record.GradeSheet!.Status != GradeSheetStatus.Published)
            return ConflictProblem("只能修改已发布的成绩。");
        var gm = new GradeModification { GradeRecordId = req.GradeRecordId, CurrentScore = record.TotalScore ?? 0, RequestedScore = req.RequestedScore, Reason = req.Reason.Trim(), ApplicantUserId = scope.Current.UserId };
        db.GradeModifications.Add(gm);
        await db.SaveChangesAsync(ct);
        var gmInfo = await db.GradeModifications.Where(x => x.Id == gm.Id)
            .Select(x => new { StudentName = x.GradeRecord!.Student!.Name, CourseName = x.GradeRecord.GradeSheet!.TeachingTask!.Course!.Name, x.CurrentScore, x.RequestedScore })
            .FirstAsync(ct);
        await NotificationService.SendToRoleAsync(db, SystemRoles.CollegeAdmin, "成绩修改待审核",
            $"{gmInfo.StudentName} — 《{gmInfo.CourseName}》{gmInfo.CurrentScore}→{gmInfo.RequestedScore}",
            cancellationToken: ct,
            category: NotificationCategory.Grade);
        return Created("", new { gm.Id });
    }

    [HttpPost("grade-modifications/{id:guid}/college-approve")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> CollegeApproveGradeMod(Guid id, CancellationToken ct)
    {
        var gm = await db.GradeModifications.Include(x => x.GradeRecord).ThenInclude(x => x!.GradeSheet).ThenInclude(x => x!.TeachingTask).ThenInclude(x => x!.Course).FirstOrDefaultAsync(x => x.Id == id, ct);
        if (gm is null) return NotFound();
        if (gm.Status != GradeModificationStatus.TeacherSubmitted) return ConflictProblem("状态不正确。");
        gm.Status = GradeModificationStatus.CollegeApproved; gm.CollegeReviewedAt = DateTime.UtcNow; gm.CollegeReviewedByUserId = scope.Current.UserId;
        await db.SaveChangesAsync(ct);
        await NotificationService.SendToRoleAsync(
            db,
            SystemRoles.AcademicAdmin,
            "成绩修改待校级审核",
            $"{gm.GradeRecord!.Student!.Name} — 《{gm.GradeRecord.GradeSheet!.TeachingTask!.Course!.Name}》",
            linkUrl: "/approvals",
            cancellationToken: ct,
            category: NotificationCategory.Grade);
        return NoContent();
    }

    [HttpPost("grade-modifications/{id:guid}/approve")]
    [Authorize(Roles = SystemRoles.SuperAdmin + "," + SystemRoles.AcademicAdmin)]
    public async Task<ActionResult> FinalApproveGradeMod(Guid id, CancellationToken ct)
    {
        var gm = await db.GradeModifications
            .Include(x => x.GradeRecord)
            .ThenInclude(x => x!.Student)
            .Include(x => x.GradeRecord)
            .ThenInclude(x => x!.GradeSheet)
            .ThenInclude(x => x!.TeachingTask)
            .ThenInclude(x => x!.Course)
            .FirstOrDefaultAsync(x => x.Id == id, ct);
        if (gm is null) return NotFound();
        if (gm.Status != GradeModificationStatus.CollegeApproved) return ConflictProblem("需先通过学院审核。");
        gm.Status = GradeModificationStatus.Approved; gm.FinalReviewedAt = DateTime.UtcNow; gm.FinalReviewedByUserId = scope.Current.UserId;
        // Auto-apply: update grade record
        gm.GradeRecord!.TotalScore = gm.RequestedScore;
        gm.GradeRecord.GradePoint = GradeCalculator.CalculateGradePoint(gm.RequestedScore);
        await db.SaveChangesAsync(ct);
        await NotificationService.SendAsync(
            db,
            gm.ApplicantUserId,
            "成绩修改已通过",
            "您的成绩修改申请已通过三级审批并生效。",
            "/approvals",
            ct,
            NotificationCategory.Grade);
        var studentUserId = gm.GradeRecord.Student?.UserId;
        if (studentUserId.HasValue &&
            studentUserId.Value != gm.ApplicantUserId)
        {
            await NotificationService.SendAsync(
                db,
                studentUserId.Value,
                "成绩已更新",
                $"《{gm.GradeRecord.GradeSheet!.TeachingTask!.Course!.Name}》成绩已由 {gm.CurrentScore} 调整为 {gm.RequestedScore}。",
                "/grades",
                ct,
                NotificationCategory.Grade);
        }
        return NoContent();
    }

    [HttpPost("grade-modifications/{id:guid}/reject")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> RejectGradeMod(Guid id, [FromBody] ReviewBody body, CancellationToken ct)
    {
        var gm = await db.GradeModifications.FindAsync([id], ct);
        if (gm is null) return NotFound();
        gm.Status = GradeModificationStatus.Rejected; gm.ReviewComment = body.Comment?.Trim();
        if (gm.Status == GradeModificationStatus.TeacherSubmitted) { gm.CollegeReviewedAt = DateTime.UtcNow; gm.CollegeReviewedByUserId = scope.Current.UserId; }
        else { gm.FinalReviewedAt = DateTime.UtcNow; gm.FinalReviewedByUserId = scope.Current.UserId; }
        await db.SaveChangesAsync(ct);
        await NotificationService.SendAsync(
            db,
            gm.ApplicantUserId,
            "成绩修改已驳回",
            gm.ReviewComment ?? "审核未通过。",
            "/approvals",
            ct,
            NotificationCategory.Grade);
        return NoContent();
    }

    // ═══════════════ Course Substitution ═══════════════
    [HttpGet("substitutions/mine")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> GetMySubstitutions(CancellationToken ct)
    {
        var sid = await GetStudentIdAsync(ct);
        if (sid is null) return StudentNotFound();
        return Ok(await db.CourseSubstitutions.AsNoTracking()
            .Where(x => x.StudentId == sid)
            .OrderByDescending(x => x.CreatedAt)
            .Select(x => new StudentCourseSubstitutionRecord(
                x.Id,
                x.Status,
                x.Reason,
                x.ReviewComment,
                x.SubmittedAt,
                x.OriginalCourse!.Code,
                x.OriginalCourse.Name,
                x.SubstituteCourse!.Code,
                x.SubstituteCourse.Name))
            .ToListAsync(ct));
    }

    [HttpPost("substitutions")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> ApplySubstitution(SubstitutionRequest req, CancellationToken ct)
    {
        var sid = await GetStudentIdAsync(ct);
        if (sid is null) return StudentNotFound();
        if (req.OriginalCourseId == req.SubstituteCourseId)
            return ConflictProblem("被替代课程和替代课程不能相同。");
        var isAssignedCourse = await db.TeachingTasks.AsNoTracking()
            .AnyAsync(task =>
                task.CourseId == req.OriginalCourseId &&
                task.Status == TeachingTaskStatus.Published &&
                !task.AcademicTerm!.IsArchived &&
                (task.AcademicTerm.IsCurrent ||
                 db.ScheduleEntries.Any(entry =>
                     entry.TeachingTaskId == task.Id &&
                     entry.SchedulePlan!.Status ==
                         SchedulePlanStatus.Published)) &&
                (task.Classes.Any(item =>
                     item.AdministrativeClass!.Students.Any(student =>
                         student.Id == sid.Value)) ||
                 db.CourseEnrollments.Any(enrollment =>
                     enrollment.StudentId == sid.Value &&
                     enrollment.Status == CourseEnrollmentStatus.Enrolled &&
                     enrollment.CourseSelectionOffering!.TeachingTaskId ==
                         task.Id)),
                ct);
        var hasFailedOriginal = await db.GradeRecords.AsNoTracking()
            .AnyAsync(record =>
                record.StudentId == sid.Value &&
                record.GradeSheet!.Status == GradeSheetStatus.Published &&
                record.GradeSheet.TeachingTask!.CourseId ==
                    req.OriginalCourseId &&
                record.TotalScore < 60,
                ct);
        var canReplaceOriginal = isAssignedCourse || hasFailedOriginal;
        if (!canReplaceOriginal)
            return ConflictProblem("被替代课程必须是当前或已发布课表的修读课程，或已有未通过成绩的课程。");
        var hasPassedSubstitute = await db.GradeRecords.AsNoTracking()
            .AnyAsync(x =>
                x.StudentId == sid.Value &&
                x.GradeSheet!.Status == GradeSheetStatus.Published &&
                x.GradeSheet.TeachingTask!.CourseId == req.SubstituteCourseId &&
                x.TotalScore >= 60,
                ct);
        if (!hasPassedSubstitute)
            return ConflictProblem("替代课程必须具有已发布且及格的成绩。");
        if (await db.CourseSubstitutions.AnyAsync(x => x.StudentId == sid && x.OriginalCourseId == req.OriginalCourseId && x.Status == ApprovalStatus.Submitted, ct))
            return ConflictProblem("该课程已有替代申请在审核中。");
        var cs = new CourseSubstitution { StudentId = sid.Value, OriginalCourseId = req.OriginalCourseId, SubstituteCourseId = req.SubstituteCourseId, Reason = req.Reason.Trim() };
        db.CourseSubstitutions.Add(cs);
        await db.SaveChangesAsync(ct);
        var info = await db.CourseSubstitutions.Where(x => x.Id == cs.Id)
            .Select(x => new { StudentName = x.Student!.Name, SubName = x.SubstituteCourse!.Name, OrigName = x.OriginalCourse!.Name })
            .FirstAsync(ct);
        await NotifyManagers("课程替代申请", $"{info.StudentName} 申请用《{info.SubName}》替代《{info.OrigName}》", ct);
        return Created("", new { cs.Id });
    }

    [HttpPost("substitutions/{id:guid}/approve")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> ApproveSubstitution(Guid id, CancellationToken ct)
    {
        var cs = await db.CourseSubstitutions.Include(x => x.Student).Include(x => x.OriginalCourse).Include(x => x.SubstituteCourse).FirstOrDefaultAsync(x => x.Id == id, ct);
        if (cs is null) return NotFound();
        cs.Status = ApprovalStatus.Approved; cs.ReviewedAt = DateTime.UtcNow; cs.ReviewedByUserId = scope.Current.UserId;
        // Auto-apply: copy passed grade to original course
        var currentTerm = await db.AcademicTerms.FirstOrDefaultAsync(x => x.IsCurrent, ct);
        if (currentTerm is not null)
        {
            var passedRecord = await db.GradeRecords
                .Where(x => x.StudentId == cs.StudentId && x.GradeSheet!.TeachingTask!.CourseId == cs.SubstituteCourseId && x.TotalScore >= 60)
                .OrderByDescending(x => x.GradeSheet!.TeachingTask!.AcademicTerm!.StartDate)
                .FirstOrDefaultAsync(ct);
            if (passedRecord is not null)
            {
                // Find or create grade record for original course
                var origTask = await db.TeachingTasks.FirstOrDefaultAsync(x => x.CourseId == cs.OriginalCourseId && x.AcademicTermId == currentTerm.Id, ct);
                if (origTask is not null)
                {
                    var origSheet = await db.GradeSheets.FirstOrDefaultAsync(x => x.TeachingTaskId == origTask.Id, ct);
                    if (origSheet is not null)
                    {
                        var origRecord = await db.GradeRecords.FirstOrDefaultAsync(x => x.GradeSheetId == origSheet.Id && x.StudentId == cs.StudentId, ct);
                        if (origRecord is not null)
                        {
                            origRecord.TotalScore = passedRecord.TotalScore;
                            origRecord.GradePoint = passedRecord.GradePoint;
                            origRecord.Notes =
                                $"课程替代：{cs.SubstituteCourse!.Code} {cs.SubstituteCourse.Name}";
                        }
                    }
                }
            }
        }
        await db.SaveChangesAsync(ct);
        await NotifyStudent(cs.StudentId, "课程替代申请已通过", $"《{cs.SubstituteCourse!.Name}》替代《{cs.OriginalCourse!.Name}》已生效。", ct);
        return NoContent();
    }

    [HttpPost("substitutions/{id:guid}/reject")]
    [Authorize(Roles = Managers)]
    public async Task<ActionResult> RejectSubstitution(Guid id, [FromBody] ReviewBody body, CancellationToken ct)
    {
        var cs = await db.CourseSubstitutions.FindAsync([id], ct);
        if (cs is null) return NotFound();
        cs.Status = ApprovalStatus.Rejected; cs.ReviewedAt = DateTime.UtcNow; cs.ReviewComment = body.Comment?.Trim(); cs.ReviewedByUserId = scope.Current.UserId;
        await db.SaveChangesAsync(ct);
        await NotifyStudent(cs.StudentId, "课程替代申请已驳回", cs.ReviewComment ?? "审核未通过。", ct);
        return NoContent();
    }

    // ═══════════════ Student course options ═══════════════
    [HttpGet("my-courses")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> GetMyEnrolledCourses(CancellationToken ct)
    {
        var sid = await GetStudentIdAsync(ct);
        if (sid is null) return StudentNotFound();
        var courses = await db.TeachingTasks.AsNoTracking()
            .Where(task =>
                task.Status == TeachingTaskStatus.Published &&
                !task.AcademicTerm!.IsArchived &&
                (task.AcademicTerm.IsCurrent ||
                 db.ScheduleEntries.Any(entry =>
                     entry.TeachingTaskId == task.Id &&
                     entry.SchedulePlan!.Status ==
                         SchedulePlanStatus.Published)) &&
                (task.Classes.Any(item =>
                     item.AdministrativeClass!.Students.Any(student =>
                         student.Id == sid.Value)) ||
                 db.CourseEnrollments.Any(enrollment =>
                     enrollment.StudentId == sid.Value &&
                     enrollment.Status == CourseEnrollmentStatus.Enrolled &&
                     enrollment.CourseSelectionOffering!.TeachingTaskId ==
                         task.Id)))
            .OrderByDescending(x => x.AcademicTerm!.IsCurrent)
            .ThenByDescending(x => x.AcademicTerm!.StartDate)
            .ThenBy(x => x.Course!.Code)
            .ThenBy(x => x.TaskNumber)
            .Select(x => new StudentApprovalCourseOption(
                x.Id,
                x.CourseId,
                x.TaskNumber,
                x.Course!.Code,
                x.Course.Name,
                x.Course.Credits,
                x.AcademicTerm!.Name,
                x.Teachers
                    .OrderByDescending(item => item.IsPrimary)
                    .ThenBy(item => item.Teacher!.Name)
                    .Select(item => item.Teacher!.Name),
                db.ScheduleEntries.Any(entry =>
                    entry.TeachingTaskId == x.Id &&
                    entry.SchedulePlan!.Status == SchedulePlanStatus.Published),
                x.SchedulingMode))
            .ToListAsync(ct);
        return Ok(courses);
    }

    [HttpGet("my-grades")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> GetMyGrades(CancellationToken ct)
    {
        var sid = await GetStudentIdAsync(ct);
        if (sid is null) return StudentNotFound();
        var grades = await db.GradeRecords.AsNoTracking()
            .Where(x => x.StudentId == sid && x.GradeSheet!.Status == GradeSheetStatus.Published)
            .OrderByDescending(x => x.GradeSheet!.PublishedAt)
            .Select(x => new StudentApprovalGradeOption(
                x.GradeSheet!.TeachingTask!.CourseId,
                x.GradeSheet.TeachingTask.Course!.Code,
                x.GradeSheet.TeachingTask.Course.Name,
                x.GradeSheet.TeachingTask.Course.Credits,
                x.TotalScore,
                x.GradePoint,
                x.ExamStatus,
                x.GradeSheet.TeachingTask.AcademicTerm!.Name,
                x.GradeSheet.TeachingTask.TaskNumber,
                x.GradeSheet.PublishedAt))
            .ToListAsync(ct);
        return Ok(grades);
    }

    [HttpGet("grade-records")]
    [Authorize(Roles = Teachers)]
    public async Task<ActionResult> GetGradeRecordsForMod(CancellationToken ct)
    {
        var userId = scope.Current.UserId;
        var records = await db.GradeRecords.AsNoTracking()
            .Where(x => x.GradeSheet!.Status == GradeSheetStatus.Published && x.GradeSheet.TeachingTask!.Teachers.Any(t => t.Teacher!.UserId == userId))
            .Select(x => new { x.Id, StudentName = x.Student!.Name, StudentNumber = x.Student.StudentNumber, CourseName = x.GradeSheet!.TeachingTask!.Course!.Name, x.TotalScore })
            .ToListAsync(ct);
        return Ok(records);
    }

    // ═══════════════ Helpers ═══════════════
    private IQueryable<T> Scoped<T>(System.Linq.Expressions.Expression<Func<T, bool>> predicate) where T : class
    {
        var q = db.Set<T>().AsNoTracking().Where(predicate);
        if (scope.Current.Scope == DataScope.All) return q;
        var cid = scope.Current.RestrictedCollegeId;
        if (typeof(T) == typeof(CourseExemption)) return (IQueryable<T>)((IQueryable<CourseExemption>)(object)q).Where(x => x.TeachingTask!.Course!.CollegeId == cid);
        if (typeof(T) == typeof(DeferredExam)) return (IQueryable<T>)((IQueryable<DeferredExam>)(object)q).Where(x => x.TeachingTask!.Course!.CollegeId == cid);
        if (typeof(T) == typeof(GradeModification)) return (IQueryable<T>)((IQueryable<GradeModification>)(object)q).Where(x => x.GradeRecord!.GradeSheet!.TeachingTask!.Course!.CollegeId == cid);
        if (typeof(T) == typeof(CourseSubstitution)) return (IQueryable<T>)((IQueryable<CourseSubstitution>)(object)q).Where(x => x.Student!.AdministrativeClass!.Major!.CollegeId == cid);
        if (typeof(T) == typeof(StudentStatusChange)) return (IQueryable<T>)((IQueryable<StudentStatusChange>)(object)q).Where(x => x.Student!.AdministrativeClass!.Major!.CollegeId == cid);
        if (typeof(T) == typeof(CourseAdjustment)) return (IQueryable<T>)((IQueryable<CourseAdjustment>)(object)q).Where(x => x.TeachingTask!.Course!.CollegeId == cid);
        if (typeof(T) == typeof(GradeSheet)) return (IQueryable<T>)((IQueryable<GradeSheet>)(object)q).Where(x => x.TeachingTask!.Course!.CollegeId == cid);
        if (typeof(T) == typeof(AttendanceRecord)) return (IQueryable<T>)((IQueryable<AttendanceRecord>)(object)q).Where(x => x.Student!.AdministrativeClass!.Major!.CollegeId == cid);
        return q;
    }

    private async Task<Guid?> GetStudentIdAsync(CancellationToken ct) => await db.Students.Where(s => s.UserId == scope.Current.UserId).Select(s => (Guid?)s.Id).FirstOrDefaultAsync(ct);
    private async Task<bool> IsStudentCourseAvailableForApplicationAsync(
        Guid studentId,
        Guid teachingTaskId,
        CancellationToken ct) =>
        await db.TeachingTasks.AsNoTracking().AnyAsync(task =>
            task.Id == teachingTaskId &&
            task.Status == TeachingTaskStatus.Published &&
            !task.AcademicTerm!.IsArchived &&
            (task.AcademicTerm.IsCurrent ||
             db.ScheduleEntries.Any(entry =>
                 entry.TeachingTaskId == task.Id &&
                 entry.SchedulePlan!.Status == SchedulePlanStatus.Published)) &&
            (task.Classes.Any(item =>
                 item.AdministrativeClass!.Students.Any(student =>
                     student.Id == studentId)) ||
             db.CourseEnrollments.Any(enrollment =>
                 enrollment.StudentId == studentId &&
                 enrollment.Status == CourseEnrollmentStatus.Enrolled &&
                 enrollment.CourseSelectionOffering!.TeachingTaskId ==
                     task.Id)),
            ct);
    private ActionResult StudentNotFound() => Conflict(new ProblemDetails { Title = "未关联学生档案", Detail = "当前账号未关联有效学生档案。", Status = 409 });

    private async Task NotifyManagers(string title, string content, CancellationToken ct) { await NotificationService.SendToRoleAsync(db, SystemRoles.CollegeAdmin, title, content, cancellationToken: ct, category: NotificationCategory.Approval); await NotificationService.SendToRoleAsync(db, SystemRoles.AcademicAdmin, title, content, cancellationToken: ct, category: NotificationCategory.Approval); }
    private async Task NotifyStudent(Guid sid, string title, string content, CancellationToken ct) { var uid = await db.Students.Where(s => s.Id == sid).Select(s => s.UserId).FirstOrDefaultAsync(ct); if (uid.HasValue) await NotificationService.SendAsync(db, uid.Value, title, content, "/approvals", ct, NotificationCategory.Approval); }
    private async Task NotifyCollege(GradeModification gm, CancellationToken ct) { await NotificationService.SendToRoleAsync(db, SystemRoles.CollegeAdmin, "成绩修改待审核", $"{gm.GradeRecord!.Student!.Name} — 《{gm.GradeRecord.GradeSheet!.TeachingTask!.Course!.Name}》{gm.CurrentScore}→{gm.RequestedScore}", cancellationToken: ct, category: NotificationCategory.Grade); }
    private async Task NotifyRole(string role, string title, string content, CancellationToken ct) { await NotificationService.SendToRoleAsync(db, role, title, content, cancellationToken: ct, category: NotificationCategory.Approval); }

    private static string SSCLabel(StudentStatusChangeType t) => t switch { StudentStatusChangeType.Suspension => "休学", StudentStatusChangeType.Resumption => "复学", StudentStatusChangeType.Withdrawal => "退学", _ => "异动" };
    private static string CALabel(CourseAdjustmentType t) => t switch { CourseAdjustmentType.Reschedule => "调课", CourseAdjustmentType.Cancel => "停课", CourseAdjustmentType.Makeup => "补课", CourseAdjustmentType.Substitute => "代课", _ => "调停课" };
    private ActionResult ConflictProblem(string d) => Conflict(new ProblemDetails { Title = "无法完成", Detail = d, Status = 409 });
}

public sealed record ApprovalItem(Guid Id, string Type, string Label, string Title, string Desc, DateTime Time, string College);
public sealed record StudentApprovalCourseOption(
    Guid TeachingTaskId,
    Guid CourseId,
    string TaskNumber,
    string CourseCode,
    string CourseName,
    decimal Credits,
    string AcademicTermName,
    IEnumerable<string> TeacherNames,
    bool HasPublishedSchedule,
    TeachingTaskSchedulingMode SchedulingMode);
public sealed record StudentApprovalGradeOption(
    Guid CourseId,
    string CourseCode,
    string CourseName,
    decimal Credits,
    decimal? TotalScore,
    decimal? GradePoint,
    GradeExamStatus ExamStatus,
    string AcademicTermName,
    string TaskNumber,
    DateTime? PublishedAt);
public sealed record StudentCourseSubstitutionRecord(
    Guid Id,
    ApprovalStatus Status,
    string Reason,
    string? ReviewComment,
    DateTime SubmittedAt,
    string OriginalCourseCode,
    string OriginalCourseName,
    string SubstituteCourseCode,
    string SubstituteCourseName);
public sealed record ExemptionRequest(Guid TeachingTaskId, [Required, MaxLength(500)] string Reason);
public sealed record GradeModRequest(Guid GradeRecordId, decimal RequestedScore, [Required, MaxLength(500)] string Reason);
public sealed record SubstitutionRequest(Guid OriginalCourseId, Guid SubstituteCourseId, [Required, MaxLength(500)] string Reason);
public sealed record ReviewBody([MaxLength(500)] string? Comment);
