using System.ComponentModel.DataAnnotations;
using Jiaowu.Api.Contracts;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Common;
using Jiaowu.Api.Domain.Identity;
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/base-data")]
public sealed class BaseDataController(AppDbContext db, IAppCache cache) : ControllerBase
{
    private const string Administrators =
        $"{SystemRoles.SuperAdmin},{SystemRoles.AcademicAdmin}";

    [HttpGet("campuses")]
    public async Task<ActionResult<IReadOnlyCollection<Campus>>> GetCampuses(
        CancellationToken cancellationToken) =>
        await cache.GetOrCreateAsync(
            AppCacheKeys.BaseData("campuses"),
            token => db.Campuses.AsNoTracking()
                .OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
                .ToListAsync(token),
            AppCacheProfile.ReferenceData,
            [AppCacheTags.BaseData],
            cancellationToken);

    [HttpPost("campuses")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<Campus>> CreateCampus(
        CatalogRequest request,
        CancellationToken cancellationToken)
    {
        var entity = new Campus
        {
            Code = request.Code.Trim(),
            Name = request.Name.Trim(),
            Address = request.Description?.Trim(),
            SortOrder = request.SortOrder,
            IsEnabled = request.IsEnabled
        };
        return await CreateAsync(entity, "GetCampuses", cancellationToken);
    }

    [HttpPut("campuses/{id:guid}")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<Campus>> UpdateCampus(
        Guid id,
        CatalogRequest request,
        CancellationToken cancellationToken)
    {
        var entity = await db.Campuses.FindAsync([id], cancellationToken);
        if (entity is null) return NotFound();
        ApplyCatalog(entity, request);
        entity.Address = request.Description?.Trim();
        await SaveAndInvalidateAsync(cancellationToken);
        return entity;
    }

    [HttpGet("colleges")]
    public async Task<ActionResult<object>> GetColleges(CancellationToken cancellationToken)
    {
        var result = await cache.GetOrCreateAsync(
            AppCacheKeys.BaseData("colleges"),
            token => db.Colleges.AsNoTracking()
                .OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
                .Select(x => new CollegeListItem(
                    x.Id,
                    x.Code,
                    x.Name,
                    x.ShortName,
                    x.CampusId,
                    x.Campus != null ? x.Campus.Name : null,
                    x.IsEnabled,
                    x.SortOrder))
                .ToListAsync(token),
            AppCacheProfile.ReferenceData,
            [AppCacheTags.BaseData],
            cancellationToken);
        return Ok(result);
    }

    [HttpPost("colleges")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<College>> CreateCollege(
        CollegeRequest request,
        CancellationToken cancellationToken)
    {
        if (request.CampusId.HasValue &&
            !await db.Campuses.AnyAsync(x => x.Id == request.CampusId, cancellationToken))
        {
            return ValidationProblem("所选校区不存在。");
        }

        var entity = new College
        {
            Code = request.Code.Trim(),
            Name = request.Name.Trim(),
            ShortName = request.ShortName?.Trim(),
            CampusId = request.CampusId,
            SortOrder = request.SortOrder,
            IsEnabled = request.IsEnabled
        };
        return await CreateAsync(entity, "GetColleges", cancellationToken);
    }

    [HttpPut("colleges/{id:guid}")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<College>> UpdateCollege(
        Guid id,
        CollegeRequest request,
        CancellationToken cancellationToken)
    {
        var entity = await db.Colleges.FindAsync([id], cancellationToken);
        if (entity is null) return NotFound();
        ApplyCatalog(entity, request);
        entity.ShortName = request.ShortName?.Trim();
        entity.CampusId = request.CampusId;
        await SaveAndInvalidateAsync(cancellationToken);
        return entity;
    }

    [HttpGet("majors")]
    public async Task<ActionResult<object>> GetMajors(CancellationToken cancellationToken)
    {
        var result = await cache.GetOrCreateAsync(
            AppCacheKeys.BaseData("majors"),
            token => db.Majors.AsNoTracking()
                .OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
                .Select(x => new MajorListItem(
                    x.Id,
                    x.Code,
                    x.Name,
                    x.CollegeId,
                    x.College!.Name,
                    x.DegreeType,
                    x.SchoolingYears,
                    x.IsEnabled,
                    x.SortOrder))
                .ToListAsync(token),
            AppCacheProfile.ReferenceData,
            [AppCacheTags.BaseData],
            cancellationToken);
        return Ok(result);
    }

    [HttpGet("course-categories")]
    public async Task<ActionResult<IReadOnlyCollection<CourseCategory>>> GetCourseCategories(
        CancellationToken cancellationToken) =>
        await cache.GetOrCreateAsync(
            AppCacheKeys.BaseData("course-categories"),
            token => db.CourseCategories.AsNoTracking()
                .OrderBy(x => x.SortOrder)
                .ThenBy(x => x.Code)
                .ToListAsync(token),
            AppCacheProfile.ReferenceData,
            [AppCacheTags.BaseData],
            cancellationToken);

    [HttpPost("course-categories")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<CourseCategory>> CreateCourseCategory(
        CatalogRequest request,
        CancellationToken cancellationToken)
    {
        var entity = new CourseCategory
        {
            Code = request.Code.Trim(),
            Name = request.Name.Trim(),
            SortOrder = request.SortOrder,
            IsEnabled = request.IsEnabled
        };
        return await CreateAsync(entity, nameof(GetCourseCategories), cancellationToken);
    }

    [HttpPut("course-categories/{id:guid}")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<CourseCategory>> UpdateCourseCategory(
        Guid id,
        CatalogRequest request,
        CancellationToken cancellationToken)
    {
        var entity = await db.CourseCategories.FindAsync([id], cancellationToken);
        if (entity is null) return NotFound();
        ApplyCatalog(entity, request);
        await SaveAndInvalidateAsync(cancellationToken);
        return entity;
    }

    [HttpPost("majors")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<Major>> CreateMajor(
        MajorRequest request,
        CancellationToken cancellationToken)
    {
        if (!await db.Colleges.AnyAsync(x => x.Id == request.CollegeId, cancellationToken))
            return ValidationProblem("所选学院不存在。");

        var entity = new Major
        {
            Code = request.Code.Trim(),
            Name = request.Name.Trim(),
            CollegeId = request.CollegeId,
            DegreeType = request.DegreeType.Trim(),
            SchoolingYears = request.SchoolingYears,
            SortOrder = request.SortOrder,
            IsEnabled = request.IsEnabled
        };
        return await CreateAsync(entity, "GetMajors", cancellationToken);
    }

    [HttpPut("majors/{id:guid}")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<Major>> UpdateMajor(
        Guid id,
        MajorRequest request,
        CancellationToken cancellationToken)
    {
        var entity = await db.Majors.FindAsync([id], cancellationToken);
        if (entity is null) return NotFound();
        ApplyCatalog(entity, request);
        entity.CollegeId = request.CollegeId;
        entity.DegreeType = request.DegreeType.Trim();
        entity.SchoolingYears = request.SchoolingYears;
        await SaveAndInvalidateAsync(cancellationToken);
        return entity;
    }

    [HttpGet("classes")]
    public async Task<ActionResult<object>> GetClasses(
        int? page,
        int? pageSize,
        string? keyword,
        Guid? collegeId,
        Guid? majorId,
        int? grade,
        bool? isEnabled,
        CancellationToken cancellationToken)
    {
        if (page.HasValue || pageSize.HasValue)
        {
            if (page is < 1 || pageSize is < 1 or > 100)
                return ValidationProblem("页码必须大于 0，且每页条数应在 1 至 100 之间。");

            var source = db.AdministrativeClasses.AsNoTracking();
            if (collegeId.HasValue)
                source = source.Where(x => x.Major!.CollegeId == collegeId.Value);
            if (majorId.HasValue) source = source.Where(x => x.MajorId == majorId.Value);
            if (grade.HasValue) source = source.Where(x => x.Grade == grade.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.Major!.Name.Contains(keyword) ||
                    x.Major.College!.Name.Contains(keyword));
            }
            var total = await source.CountAsync(cancellationToken);
            var items = await source
                .OrderByDescending(x => x.Grade).ThenBy(x => x.Code)
                .Skip((page!.Value - 1) * pageSize!.Value)
                .Take(pageSize.Value)
                .Select(x => new AdministrativeClassListItem(
                    x.Id, x.Code, x.Name, x.MajorId, x.Major!.Name,
                    x.Major.College!.Name, x.Grade, x.CounselorUserId,
                    x.CounselorName, x.IsEnabled, x.SortOrder))
                .ToListAsync(cancellationToken);
            return Ok(new PagedResult<AdministrativeClassListItem>(
                items, total, page.Value, pageSize.Value));
        }

        var result = await cache.GetOrCreateAsync(
            AppCacheKeys.BaseData("classes"),
            token => db.AdministrativeClasses.AsNoTracking()
                .OrderByDescending(x => x.Grade).ThenBy(x => x.Code)
                .Select(x => new AdministrativeClassListItem(
                    x.Id,
                    x.Code,
                    x.Name,
                    x.MajorId,
                    x.Major!.Name,
                    x.Major.College!.Name,
                    x.Grade,
                    x.CounselorUserId,
                    x.CounselorName,
                    x.IsEnabled,
                    x.SortOrder))
                .ToListAsync(token),
            AppCacheProfile.ReferenceData,
            [AppCacheTags.BaseData],
            cancellationToken);
        return Ok(result);
    }

    [HttpGet("counselors")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<object>> GetCounselors(CancellationToken cancellationToken) =>
        Ok(await db.Users.AsNoTracking()
            .Where(user =>
                user.IsEnabled &&
                db.UserRoles.Any(userRole =>
                    userRole.UserId == user.Id &&
                    db.Roles.Any(role =>
                        role.Id == userRole.RoleId &&
                        role.Name == SystemRoles.Counselor)))
            .OrderBy(x => x.DisplayName)
            .Select(x => new
            {
                x.Id,
                x.DisplayName,
                x.StaffNumber,
                x.CollegeId
            })
            .ToListAsync(cancellationToken));

    [HttpPost("classes")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<AdministrativeClass>> CreateClass(
        ClassRequest request,
        CancellationToken cancellationToken)
    {
        if (!await db.Majors.AnyAsync(x => x.Id == request.MajorId, cancellationToken))
            return ValidationProblem("所选专业不存在。");
        var counselorName = await GetCounselorNameAsync(
            request.CounselorUserId,
            cancellationToken);
        if (request.CounselorUserId.HasValue && counselorName is null)
            return ValidationProblem("所选辅导员账号不存在、已停用或没有辅导员角色。");

        var entity = new AdministrativeClass
        {
            Code = request.Code.Trim(),
            Name = request.Name.Trim(),
            MajorId = request.MajorId,
            Grade = request.Grade,
            CounselorUserId = request.CounselorUserId,
            CounselorName = counselorName,
            SortOrder = request.SortOrder,
            IsEnabled = request.IsEnabled
        };
        return await CreateAsync(entity, "GetClasses", cancellationToken);
    }

    [HttpPut("classes/{id:guid}")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<AdministrativeClass>> UpdateClass(
        Guid id,
        ClassRequest request,
        CancellationToken cancellationToken)
    {
        var entity = await db.AdministrativeClasses.FindAsync([id], cancellationToken);
        if (entity is null) return NotFound();
        var counselorName = await GetCounselorNameAsync(
            request.CounselorUserId,
            cancellationToken);
        if (request.CounselorUserId.HasValue && counselorName is null)
            return ValidationProblem("所选辅导员账号不存在、已停用或没有辅导员角色。");
        ApplyCatalog(entity, request);
        entity.MajorId = request.MajorId;
        entity.Grade = request.Grade;
        entity.CounselorUserId = request.CounselorUserId;
        entity.CounselorName = counselorName;
        await SaveAndInvalidateAsync(cancellationToken);
        return entity;
    }

    [HttpGet("terms")]
    public async Task<ActionResult<IReadOnlyCollection<AcademicTerm>>> GetTerms(
        CancellationToken cancellationToken) =>
        await cache.GetOrCreateAsync(
            AppCacheKeys.BaseData("terms"),
            token => db.AcademicTerms.AsNoTracking()
                .OrderByDescending(x => x.IsCurrent)
                .ThenBy(x => x.IsArchived)
                .ThenByDescending(x => x.StartDate)
                .ToListAsync(token),
            AppCacheProfile.ReferenceData,
            [AppCacheTags.BaseData],
            cancellationToken);

    [HttpPost("terms")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<AcademicTerm>> CreateTerm(
        TermRequest request,
        CancellationToken cancellationToken)
    {
        if (request.EndDate <= request.StartDate)
            return ValidationProblem("学期结束日期必须晚于开始日期。");
        if (request.IsCurrent && !request.IsEnabled)
            return ValidationProblem("当前学期必须保持启用。");

        var hasCurrentTerm = await db.AcademicTerms
            .AnyAsync(x => x.IsCurrent, cancellationToken);
        var shouldBeCurrent = request.IsCurrent || (!hasCurrentTerm && request.IsEnabled);
        if (shouldBeCurrent)
        {
            var currentTerms = await db.AcademicTerms
                .Where(x => x.IsCurrent)
                .ToListAsync(cancellationToken);
            foreach (var currentTerm in currentTerms)
                currentTerm.IsCurrent = false;
        }

        var entity = new AcademicTerm
        {
            Code = request.Code.Trim(),
            Name = request.Name.Trim(),
            AcademicYear = request.AcademicYear.Trim(),
            Season = request.Season,
            StartDate = request.StartDate,
            EndDate = request.EndDate,
            IsCurrent = shouldBeCurrent,
            IsEnabled = request.IsEnabled
        };
        return await CreateAsync(entity, "GetTerms", cancellationToken);
    }

    [HttpPut("terms/{id:guid}")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<AcademicTerm>> UpdateTerm(
        Guid id,
        TermRequest request,
        CancellationToken cancellationToken)
    {
        var entity = await db.AcademicTerms.FindAsync([id], cancellationToken);
        if (entity is null) return NotFound();
        if (request.EndDate <= request.StartDate)
            return ValidationProblem("学期结束日期必须晚于开始日期。");
        if (entity.IsCurrent && !request.IsCurrent)
            return ConflictProblem("不能直接取消当前学期，请将另一个学期设为当前。");
        if (request.IsCurrent && entity.IsArchived)
            return ConflictProblem("已归档学期不能直接设为当前，请先撤销归档。");
        if (request.IsCurrent && !request.IsEnabled)
            return ValidationProblem("当前学期必须保持启用。");
        if (request.IsCurrent)
        {
            var currentTerms = await db.AcademicTerms
                .Where(x => x.Id != id && x.IsCurrent)
                .ToListAsync(cancellationToken);
            foreach (var currentTerm in currentTerms)
                currentTerm.IsCurrent = false;
        }
        ApplyCatalog(entity, request);
        entity.AcademicYear = request.AcademicYear.Trim();
        entity.Season = request.Season;
        entity.StartDate = request.StartDate;
        entity.EndDate = request.EndDate;
        entity.IsCurrent = request.IsCurrent;
        await SaveAndInvalidateAsync(cancellationToken);
        return entity;
    }

    [HttpPost("terms/{id:guid}/set-current")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<AcademicTerm>> SetCurrentTerm(
        Guid id,
        CancellationToken cancellationToken)
    {
        var entity = await db.AcademicTerms.FindAsync([id], cancellationToken);
        if (entity is null) return NotFound();
        if (entity.IsArchived)
            return ConflictProblem("已归档学期不能直接设为当前，请先撤销归档。");
        if (!entity.IsEnabled)
            return ConflictProblem("停用学期不能设为当前，请先启用。");
        if (entity.IsCurrent) return entity;

        var currentTerms = await db.AcademicTerms
            .Where(x => x.Id != id && x.IsCurrent)
            .ToListAsync(cancellationToken);
        foreach (var currentTerm in currentTerms)
            currentTerm.IsCurrent = false;
        entity.IsCurrent = true;
        await SaveAndInvalidateAsync(cancellationToken);
        return entity;
    }

    [HttpPost("terms/{id:guid}/archive")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<AcademicTerm>> ArchiveTerm(
        Guid id,
        CancellationToken cancellationToken)
    {
        var entity = await db.AcademicTerms.FindAsync([id], cancellationToken);
        if (entity is null) return NotFound();
        if (entity.IsCurrent)
            return ConflictProblem("当前学期不能归档，请先切换到新的当前学期。");
        if (entity.IsArchived) return entity;

        entity.IsArchived = true;
        entity.ArchivedAt = DateTime.UtcNow;
        await SaveAndInvalidateAsync(cancellationToken);
        return entity;
    }

    [HttpPost("terms/{id:guid}/unarchive")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<AcademicTerm>> UnarchiveTerm(
        Guid id,
        CancellationToken cancellationToken)
    {
        var entity = await db.AcademicTerms.FindAsync([id], cancellationToken);
        if (entity is null) return NotFound();
        if (!entity.IsArchived) return entity;

        entity.IsArchived = false;
        entity.ArchivedAt = null;
        await SaveAndInvalidateAsync(cancellationToken);
        return entity;
    }

    [HttpGet("classrooms")]
    public async Task<ActionResult<object>> GetClassrooms(
        int? page,
        int? pageSize,
        string? keyword,
        Guid? campusId,
        Guid? buildingId,
        int? minimumCapacity,
        TeachingVenueNature? teachingVenueNature,
        bool? isEnabled,
        CancellationToken cancellationToken)
    {
        if (page.HasValue || pageSize.HasValue)
        {
            if (page is < 1 || pageSize is < 1 or > 100)
                return ValidationProblem("页码必须大于 0，且每页条数应在 1 至 100 之间。");

            var source = db.Classrooms.AsNoTracking();
            if (campusId.HasValue)
                source = source.Where(x => x.Building!.CampusId == campusId.Value);
            if (buildingId.HasValue) source = source.Where(x => x.BuildingId == buildingId.Value);
            if (minimumCapacity.HasValue)
                source = source.Where(x => x.Capacity >= minimumCapacity.Value);
            if (teachingVenueNature.HasValue)
                source = source.Where(x => (x.TeachingVenueNature & teachingVenueNature.Value) != 0);
            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.Building!.Name.Contains(keyword) ||
                    x.Building.Campus!.Name.Contains(keyword));
            }
            var total = await source.CountAsync(cancellationToken);
            var items = await source
                .OrderBy(x => x.Building!.Campus!.SortOrder).ThenBy(x => x.Code)
                .Skip((page!.Value - 1) * pageSize!.Value)
                .Take(pageSize.Value)
                .Select(x => new ClassroomListItem(
                    x.Id, x.Code, x.Name, x.BuildingId, x.Building!.CampusId,
                    x.Building.Name, x.Building.Campus!.Name, x.Capacity,
                    x.RoomType, x.TeachingVenueNature, x.Equipment,
                    x.IsEnabled, x.SortOrder))
                .ToListAsync(cancellationToken);
            return Ok(new PagedResult<ClassroomListItem>(
                items, total, page.Value, pageSize.Value));
        }

        var result = await cache.GetOrCreateAsync(
            AppCacheKeys.BaseData("classrooms"),
            token => db.Classrooms.AsNoTracking()
                .OrderBy(x => x.Building!.Campus!.SortOrder)
                .ThenBy(x => x.Code)
                .Select(x => new ClassroomListItem(
                    x.Id,
                    x.Code,
                    x.Name,
                    x.BuildingId,
                    x.Building!.CampusId,
                    x.Building.Name,
                    x.Building.Campus!.Name,
                    x.Capacity,
                    x.RoomType,
                    x.TeachingVenueNature,
                    x.Equipment,
                    x.IsEnabled,
                    x.SortOrder))
                .ToListAsync(token),
            AppCacheProfile.ReferenceData,
            [AppCacheTags.BaseData],
            cancellationToken);
        return Ok(result);
    }

    [HttpGet("buildings")]
    public async Task<ActionResult<object>> GetBuildings(CancellationToken cancellationToken)
    {
        var result = await cache.GetOrCreateAsync(
            AppCacheKeys.BaseData("buildings"),
            token => db.Buildings.AsNoTracking()
                .OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
                .Select(x => new BuildingListItem(
                    x.Id,
                    x.Code,
                    x.Name,
                    x.CampusId,
                    x.Campus!.Name,
                    x.IsEnabled,
                    x.SortOrder))
                .ToListAsync(token),
            AppCacheProfile.ReferenceData,
            [AppCacheTags.BaseData],
            cancellationToken);
        return Ok(result);
    }

    [HttpPost("buildings")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<Building>> CreateBuilding(
        BuildingRequest request,
        CancellationToken cancellationToken)
    {
        if (!await db.Campuses.AnyAsync(x => x.Id == request.CampusId, cancellationToken))
            return ValidationProblem("所选校区不存在。");
        var entity = new Building
        {
            Code = request.Code.Trim(),
            Name = request.Name.Trim(),
            CampusId = request.CampusId,
            SortOrder = request.SortOrder,
            IsEnabled = request.IsEnabled
        };
        return await CreateAsync(entity, "GetBuildings", cancellationToken);
    }

    [HttpPost("classrooms")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<Classroom>> CreateClassroom(
        ClassroomRequest request,
        CancellationToken cancellationToken)
    {
        if (!await db.Buildings.AnyAsync(x => x.Id == request.BuildingId, cancellationToken))
            return ValidationProblem("所选教学楼不存在。");
        var entity = new Classroom
        {
            Code = request.Code.Trim(),
            Name = request.Name.Trim(),
            BuildingId = request.BuildingId,
            Capacity = request.Capacity,
            RoomType = request.RoomType.Trim(),
            TeachingVenueNature = request.TeachingVenueNature == 0
                ? TeachingVenueNature.GeneralClassroom
                : request.TeachingVenueNature,
            Equipment = request.Equipment?.Trim(),
            SortOrder = request.SortOrder,
            IsEnabled = request.IsEnabled
        };
        return await CreateAsync(entity, "GetClassrooms", cancellationToken);
    }

    [HttpPut("classrooms/{id:guid}")]
    [Authorize(Roles = Administrators)]
    public async Task<ActionResult<Classroom>> UpdateClassroom(
        Guid id,
        ClassroomRequest request,
        CancellationToken cancellationToken)
    {
        var entity = await db.Classrooms.FindAsync([id], cancellationToken);
        if (entity is null) return NotFound();
        ApplyCatalog(entity, request);
        entity.BuildingId = request.BuildingId;
        entity.Capacity = request.Capacity;
        entity.RoomType = request.RoomType.Trim();
        entity.TeachingVenueNature = request.TeachingVenueNature == 0
            ? TeachingVenueNature.GeneralClassroom
            : request.TeachingVenueNature;
        entity.Equipment = request.Equipment?.Trim();
        await SaveAndInvalidateAsync(cancellationToken);
        return entity;
    }

    [HttpDelete("{kind}/{id:guid}")]
    [Authorize(Roles = Administrators)]
    public async Task<IActionResult> Delete(
        string kind,
        Guid id,
        CancellationToken cancellationToken)
    {
        object? entity = kind.ToLowerInvariant() switch
        {
            "campuses" => await db.Campuses.FindAsync([id], cancellationToken),
            "colleges" => await db.Colleges.FindAsync([id], cancellationToken),
            "majors" => await db.Majors.FindAsync([id], cancellationToken),
            "classes" => await db.AdministrativeClasses.FindAsync([id], cancellationToken),
            "terms" => await db.AcademicTerms.FindAsync([id], cancellationToken),
            "buildings" => await db.Buildings.FindAsync([id], cancellationToken),
            "classrooms" => await db.Classrooms.FindAsync([id], cancellationToken),
            "course-categories" => await db.CourseCategories.FindAsync([id], cancellationToken),
            _ => null
        };

        if (entity is null) return NotFound();
        if (entity is AcademicTerm { IsCurrent: true })
            return ConflictProblem("当前学期不能删除，请先切换到其他学期。");
        db.Remove(entity);
        try
        {
            await SaveAndInvalidateAsync(cancellationToken);
            return NoContent();
        }
        catch (DbUpdateException)
        {
            return Conflict(new ProblemDetails
            {
                Title = "无法删除",
                Detail = "该数据已被其他业务引用，请先停用，或移除关联数据后再删除。",
                Status = StatusCodes.Status409Conflict
            });
        }
    }

    private async Task<ActionResult<TEntity>> CreateAsync<TEntity>(
        TEntity entity,
        string action,
        CancellationToken cancellationToken)
        where TEntity : EntityBase
    {
        db.Add(entity);
        try
        {
            await SaveAndInvalidateAsync(cancellationToken);
        }
        catch (DbUpdateException)
        {
            ModelState.AddModelError("code", "编码已存在或关联数据无效。");
            return ValidationProblem(ModelState);
        }
        return CreatedAtAction(action, new { id = entity.Id }, entity);
    }

    private async Task SaveAndInvalidateAsync(CancellationToken cancellationToken)
    {
        await db.SaveChangesAsync(cancellationToken);
        await cache.RemoveByTagAsync(AppCacheTags.BaseData, cancellationToken);
    }

    private async Task<string?> GetCounselorNameAsync(
        Guid? userId,
        CancellationToken cancellationToken)
    {
        if (!userId.HasValue) return null;
        return await db.Users.AsNoTracking()
            .Where(user =>
                user.Id == userId &&
                user.IsEnabled &&
                db.UserRoles.Any(userRole =>
                    userRole.UserId == user.Id &&
                    db.Roles.Any(role =>
                        role.Id == userRole.RoleId &&
                        role.Name == SystemRoles.Counselor)))
            .Select(x => x.DisplayName)
            .FirstOrDefaultAsync(cancellationToken);
    }

    private static void ApplyCatalog(CatalogEntity entity, CatalogRequest request)
    {
        entity.Code = request.Code.Trim();
        entity.Name = request.Name.Trim();
        entity.SortOrder = request.SortOrder;
        entity.IsEnabled = request.IsEnabled;
    }

    private ObjectResult ConflictProblem(string detail) =>
        Conflict(new ProblemDetails
        {
            Title = "当前操作无法完成",
            Detail = detail,
            Status = StatusCodes.Status409Conflict
        });
}

public record CatalogRequest(
    [Required, MaxLength(40)] string Code,
    [Required, MaxLength(100)] string Name,
    int SortOrder = 0,
    bool IsEnabled = true,
    [MaxLength(300)] string? Description = null);

public sealed record CollegeRequest(
    string Code, string Name, int SortOrder, bool IsEnabled,
    Guid? CampusId,
    [MaxLength(50)] string? ShortName)
    : CatalogRequest(Code, Name, SortOrder, IsEnabled);

public sealed record MajorRequest(
    string Code, string Name, int SortOrder, bool IsEnabled,
    Guid CollegeId,
    [Required, MaxLength(30)] string DegreeType,
    [Range(1, 8)] int SchoolingYears)
    : CatalogRequest(Code, Name, SortOrder, IsEnabled);

public sealed record ClassRequest(
    string Code, string Name, int SortOrder, bool IsEnabled,
    Guid MajorId,
    [Range(2000, 2200)] int Grade,
    Guid? CounselorUserId)
    : CatalogRequest(Code, Name, SortOrder, IsEnabled);

public sealed record TermRequest(
    string Code, string Name, bool IsEnabled,
    [Required, MaxLength(20)] string AcademicYear,
    TermSeason Season,
    DateOnly StartDate,
    DateOnly EndDate,
    bool IsCurrent)
    : CatalogRequest(Code, Name, 0, IsEnabled);

public sealed record BuildingRequest(
    string Code, string Name, int SortOrder, bool IsEnabled,
    Guid CampusId)
    : CatalogRequest(Code, Name, SortOrder, IsEnabled);

public sealed record ClassroomRequest(
    string Code, string Name, int SortOrder, bool IsEnabled,
    Guid BuildingId,
    [Range(1, 1000)] int Capacity,
    [Required, MaxLength(40)] string RoomType,
    TeachingVenueNature TeachingVenueNature,
    [MaxLength(300)] string? Equipment)
    : CatalogRequest(Code, Name, SortOrder, IsEnabled);

public sealed record CollegeListItem(
    Guid Id,
    string Code,
    string Name,
    string? ShortName,
    Guid? CampusId,
    string? CampusName,
    bool IsEnabled,
    int SortOrder);

public sealed record MajorListItem(
    Guid Id,
    string Code,
    string Name,
    Guid CollegeId,
    string CollegeName,
    string DegreeType,
    int SchoolingYears,
    bool IsEnabled,
    int SortOrder);

public sealed record AdministrativeClassListItem(
    Guid Id,
    string Code,
    string Name,
    Guid MajorId,
    string MajorName,
    string CollegeName,
    int Grade,
    Guid? CounselorUserId,
    string? CounselorName,
    bool IsEnabled,
    int SortOrder);

public sealed record BuildingListItem(
    Guid Id,
    string Code,
    string Name,
    Guid CampusId,
    string CampusName,
    bool IsEnabled,
    int SortOrder);

public sealed record ClassroomListItem(
    Guid Id,
    string Code,
    string Name,
    Guid BuildingId,
    Guid CampusId,
    string BuildingName,
    string CampusName,
    int Capacity,
    string RoomType,
    TeachingVenueNature TeachingVenueNature,
    string? Equipment,
    bool IsEnabled,
    int SortOrder);
