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

namespace Jiaowu.Api.Controllers;

[ApiController]
[Authorize]
[Route("api/courses")]
public sealed class CoursesController(
    AppDbContext db,
    ICurrentUserDataScope currentUserDataScope,
    IAppCache cache) : ControllerBase
{
    private const string WriteRoles =
        SystemRoles.SuperAdmin + "," +
        SystemRoles.AcademicAdmin + "," +
        SystemRoles.CollegeAdmin;

    [HttpGet]
    public async Task<ActionResult<PagedResult<object>>> Get(
        int page = 1,
        int pageSize = 20,
        string? keyword = null,
        Guid? collegeId = null,
        Guid? categoryId = null,
        CourseNature? nature = null,
        bool? isEnabled = null,
        CancellationToken cancellationToken = default)
    {
        page = Math.Max(1, page);
        pageSize = Math.Clamp(pageSize, 10, 100);
        var scope = currentUserDataScope.Current;
        var canManageAll = scope.IsInRole(SystemRoles.SuperAdmin) ||
                           scope.IsInRole(SystemRoles.AcademicAdmin);
        var canManageCollegeCourses = scope.IsInRole(SystemRoles.CollegeAdmin);
        var managedCollegeId = scope.CollegeId;
        var source = ScopedCourses().AsNoTracking();
        if (collegeId.HasValue)
            source = source.Where(x => x.CollegeId == collegeId.Value);
        if (categoryId.HasValue)
            source = source.Where(x => x.CourseCategoryId == categoryId.Value);
        if (nature.HasValue)
            source = source.Where(x => x.Nature == nature.Value);
        if (isEnabled.HasValue)
            source = source.Where(x => x.IsEnabled == isEnabled.Value);
        if (!string.IsNullOrWhiteSpace(keyword))
        {
            keyword = keyword.Trim();
            source = source.Where(x =>
                x.Code.Contains(keyword) ||
                x.Name.Contains(keyword) ||
                (x.EnglishName != null && x.EnglishName.Contains(keyword)));
        }

        var total = await source.CountAsync(cancellationToken);
        var items = await source
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Code)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(x => new
            {
                x.Id,
                x.Code,
                x.Name,
                x.EnglishName,
                x.CollegeId,
                CollegeName = x.College!.Name,
                x.CourseCategoryId,
                CategoryCode = x.CourseCategory != null ? x.CourseCategory.Code : null,
                CategoryName = x.CourseCategory != null ? x.CourseCategory.Name : null,
                x.Credits,
                x.TotalHours,
                x.LectureHours,
                x.PracticeHours,
                x.Nature,
                x.AssessmentMethod,
                x.Description,
                PrerequisiteCourseIds = x.Prerequisites
                    .OrderBy(item => item.PrerequisiteCourse!.Code)
                    .Select(item => item.PrerequisiteCourseId),
                Prerequisites = x.Prerequisites
                    .OrderBy(item => item.PrerequisiteCourse!.Code)
                    .Select(item => new
                    {
                        item.PrerequisiteCourseId,
                        item.PrerequisiteCourse!.Code,
                        item.PrerequisiteCourse.Name
                    }),
                x.IsEnabled,
                x.SortOrder,
                x.CreatedAt,
                CanManage = canManageAll ||
                    canManageCollegeCourses &&
                    x.CollegeId == managedCollegeId &&
                    (x.Nature == CourseNature.MajorRequired ||
                     x.Nature == CourseNature.MajorElective ||
                     x.Nature == CourseNature.Practice)
            })
            .ToListAsync(cancellationToken);

        return Ok(new PagedResult<object>(items, total, page, pageSize));
    }

    [HttpGet("options")]
    public async Task<ActionResult<object>> GetOptions(
        Guid? collegeId = null,
        CourseNature? nature = null,
        string? keyword = null,
        CancellationToken cancellationToken = default)
    {
        var source = ScopedCourses().AsNoTracking().Where(x => x.IsEnabled);
        if (collegeId.HasValue)
            source = source.Where(x => x.CollegeId == collegeId.Value);
        if (nature.HasValue)
            source = source.Where(x => x.Nature == nature.Value);
        if (!string.IsNullOrWhiteSpace(keyword))
        {
            keyword = keyword.Trim();
            source = source.Where(x =>
                x.Code.Contains(keyword) ||
                x.Name.Contains(keyword) ||
                (x.EnglishName != null && x.EnglishName.Contains(keyword)));
        }

        return Ok(await source
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Code)
            .Select(x => new
            {
                x.Id,
                x.Code,
                x.Name,
                x.CollegeId,
                CollegeName = x.College!.Name,
                 x.CourseCategoryId,
                 CategoryName = x.CourseCategory != null ? x.CourseCategory.Name : null,
                 x.Credits,
                 x.TotalHours,
                 x.LectureHours,
                 x.PracticeHours,
                 x.Nature,
                x.AssessmentMethod
            })
            .ToListAsync(cancellationToken));
    }

    [HttpPost]
    [Authorize(Roles = WriteRoles)]
    public async Task<ActionResult> Create(
        CourseRequest request,
        CancellationToken cancellationToken)
    {
        var entity = new Course
        {
            Code = request.Code.Trim(),
            Name = request.Name.Trim(),
            EnglishName = Normalize(request.EnglishName),
            CollegeId = request.CollegeId,
            CourseCategoryId = request.CourseCategoryId,
            Credits = request.Credits,
            TotalHours = request.TotalHours,
            LectureHours = request.LectureHours,
            PracticeHours = request.PracticeHours,
            Nature = request.Nature,
            AssessmentMethod = request.AssessmentMethod,
            Description = Normalize(request.Description),
            IsEnabled = request.IsEnabled,
            SortOrder = request.SortOrder
        };
        var validation = await ValidateAsync(entity.Id, request, cancellationToken);
        if (validation is not null) return validation;

        entity.Prerequisites = NormalizePrerequisiteIds(request)
            .Select(prerequisiteId => new CoursePrerequisite
            {
                CourseId = entity.Id,
                PrerequisiteCourseId = prerequisiteId
            })
            .ToList();
        db.Courses.Add(entity);
        return await SaveAsync(entity.Id, true, cancellationToken);
    }

    [HttpPut("{id:guid}")]
    [Authorize(Roles = WriteRoles)]
    public async Task<ActionResult> Update(
        Guid id,
        CourseRequest request,
        CancellationToken cancellationToken)
    {
        var entity = await db.Courses
            .Include(x => x.Prerequisites)
            .FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
        if (entity is null) return NotFound();
        if (!CanManage(entity.CollegeId, entity.Nature)) return Forbid();
        var validation = await ValidateAsync(id, request, cancellationToken);
        if (validation is not null) return validation;

        entity.Code = request.Code.Trim();
        entity.Name = request.Name.Trim();
        entity.EnglishName = Normalize(request.EnglishName);
        entity.CollegeId = request.CollegeId;
        entity.CourseCategoryId = request.CourseCategoryId;
        entity.Credits = request.Credits;
        entity.TotalHours = request.TotalHours;
        entity.LectureHours = request.LectureHours;
        entity.PracticeHours = request.PracticeHours;
        entity.Nature = request.Nature;
        entity.AssessmentMethod = request.AssessmentMethod;
        entity.Description = Normalize(request.Description);
        entity.IsEnabled = request.IsEnabled;
        entity.SortOrder = request.SortOrder;
        var prerequisiteIds = NormalizePrerequisiteIds(request).ToHashSet();
        db.CoursePrerequisites.RemoveRange(
            entity.Prerequisites.Where(x =>
                !prerequisiteIds.Contains(x.PrerequisiteCourseId)));
        foreach (var prerequisiteId in prerequisiteIds.Except(
                     entity.Prerequisites.Select(x => x.PrerequisiteCourseId)))
        {
            entity.Prerequisites.Add(new CoursePrerequisite
            {
                CourseId = entity.Id,
                PrerequisiteCourseId = prerequisiteId
            });
        }
        return await SaveAsync(entity.Id, false, cancellationToken);
    }

    [HttpDelete("{id:guid}")]
    [Authorize(Roles = WriteRoles)]
    public async Task<ActionResult> Delete(Guid id, CancellationToken cancellationToken)
    {
        var entity = await db.Courses.FindAsync([id], cancellationToken);
        if (entity is null) return NotFound();
        if (!CanManage(entity.CollegeId, entity.Nature)) return Forbid();
        db.Courses.Remove(entity);
        return await SaveAsync(id, false, cancellationToken);
    }

    private async Task<ActionResult?> ValidateAsync(
        Guid courseId,
        CourseRequest request,
        CancellationToken cancellationToken)
    {
        if (!CanManage(request.CollegeId, request.Nature)) return Forbid();
        if (!await db.Colleges.AnyAsync(x => x.Id == request.CollegeId, cancellationToken))
            return ValidationProblem("所选学院不存在。");
        if (!await db.CourseCategories.AnyAsync(
                x => x.Id == request.CourseCategoryId && x.IsEnabled,
                cancellationToken))
            return ValidationProblem("所选课程分类不存在或已停用。");
        if (request.LectureHours + request.PracticeHours > request.TotalHours)
            return ValidationProblem("讲授学时与实践学时之和不能超过总学时。");

        var prerequisiteIds = NormalizePrerequisiteIds(request).ToArray();
        if (prerequisiteIds.Contains(courseId))
            return ValidationProblem("课程不能把自身设置为先修课程。");
        var accessiblePrerequisiteCount = await ScopedCourses()
            .CountAsync(x =>
                prerequisiteIds.Contains(x.Id) &&
                x.IsEnabled,
                cancellationToken);
        if (accessiblePrerequisiteCount != prerequisiteIds.Length)
            return ValidationProblem("包含不存在、已停用或不在当前数据范围内的先修课程。");
        if (await CreatesPrerequisiteCycleAsync(
                courseId,
                prerequisiteIds,
                cancellationToken))
            return ValidationProblem("先修关系不能形成循环依赖。");
        return null;
    }

    private async Task<bool> CreatesPrerequisiteCycleAsync(
        Guid courseId,
        IReadOnlyCollection<Guid> prerequisiteIds,
        CancellationToken cancellationToken)
    {
        if (prerequisiteIds.Count == 0) return false;

        var edges = await db.CoursePrerequisites.AsNoTracking()
            .Where(x => x.CourseId != courseId)
            .Select(x => new { x.CourseId, x.PrerequisiteCourseId })
            .ToListAsync(cancellationToken);
        var prerequisitesByCourse = edges
            .GroupBy(x => x.CourseId)
            .ToDictionary(
                group => group.Key,
                group => group.Select(x => x.PrerequisiteCourseId).ToArray());

        foreach (var prerequisiteId in prerequisiteIds)
        {
            var pending = new Stack<Guid>();
            var visited = new HashSet<Guid>();
            pending.Push(prerequisiteId);
            while (pending.TryPop(out var candidate))
            {
                if (candidate == courseId) return true;
                if (!visited.Add(candidate) ||
                    !prerequisitesByCourse.TryGetValue(candidate, out var next))
                    continue;
                foreach (var item in next) pending.Push(item);
            }
        }
        return false;
    }

    private static IEnumerable<Guid> NormalizePrerequisiteIds(CourseRequest request) =>
        (request.PrerequisiteCourseIds ?? []).Distinct();

    private IQueryable<Course> ScopedCourses()
    {
        var scope = currentUserDataScope.Current;
        var source = db.Courses.AsQueryable();
        if (scope.Scope == DataScope.All) return source;
        if (scope.Scope == DataScope.College)
        {
            return source.Where(x =>
                x.Nature == CourseNature.GeneralRequired ||
                x.Nature == CourseNature.GeneralElective ||
                x.CollegeId == scope.RestrictedCollegeId);
        }
        if (scope.Scope == DataScope.Class)
        {
            return source.Where(course => db.TeachingTasks.Any(task =>
                task.CourseId == course.Id &&
                task.Classes.Any(item =>
                    item.AdministrativeClass!.CounselorUserId == scope.UserId)));
        }

        var userId = scope.UserId;
        var isTeacher = scope.IsInRole(SystemRoles.Teacher);
        var isStudent = scope.IsInRole(SystemRoles.Student);
        return source.Where(course =>
            isTeacher && db.TeachingTasks.Any(task =>
                task.CourseId == course.Id &&
                task.Teachers.Any(item => item.Teacher!.UserId == userId)) ||
            isStudent && db.TeachingTasks.Any(task =>
                task.CourseId == course.Id &&
                task.Classes.Any(item =>
                    item.AdministrativeClass!.Students.Any(student =>
                        student.UserId == userId))));
    }

    private bool CanManage(Guid collegeId, CourseNature nature)
        => CourseMaintenancePolicy.CanManage(
            currentUserDataScope.Current,
            collegeId,
            nature);

    private async Task<ActionResult> SaveAsync(
        Guid id,
        bool created,
        CancellationToken cancellationToken)
    {
        try
        {
            await db.SaveChangesAsync(cancellationToken);
            await cache.RemoveByTagAsync(AppCacheTags.Timetables, cancellationToken);
            return created ? Created(string.Empty, new { id }) : NoContent();
        }
        catch (DbUpdateException)
        {
            return Conflict(new ProblemDetails
            {
                Title = "无法完成操作",
                Detail = "课程编码已存在，或课程已被其他教学业务引用。",
                Status = StatusCodes.Status409Conflict
            });
        }
    }

    private static string? Normalize(string? value) =>
        string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}

public sealed record CourseRequest(
    [Required, MaxLength(40)] string Code,
    [Required, MaxLength(100)] string Name,
    [MaxLength(150)] string? EnglishName,
    Guid CollegeId,
    Guid CourseCategoryId,
    [Range(typeof(decimal), "0.1", "99")] decimal Credits,
    [Range(1, 1000)] int TotalHours,
    [Range(0, 1000)] int LectureHours,
    [Range(0, 1000)] int PracticeHours,
    CourseNature Nature,
    AssessmentMethod AssessmentMethod,
    [MaxLength(1000)] string? Description,
    bool IsEnabled,
    int SortOrder,
    IReadOnlyCollection<Guid>? PrerequisiteCourseIds = null);
