using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Common;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Caching;
using Jiaowu.Api.Infrastructure.Excel;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Jiaowu.Api.Controllers;

[ApiController]
[Authorize(Roles = Administrators)]
[Route("api/base-data")]
public sealed class BaseDataExcelController(AppDbContext db, IAppCache cache) : ControllerBase
{
    private const string Administrators =
        $"{SystemRoles.SuperAdmin},{SystemRoles.AcademicAdmin}";

    private static readonly IReadOnlyDictionary<string, string[]> Headers =
        new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase)
        {
            ["campuses"] = ["编码", "名称", "地址", "排序", "状态"],
            ["colleges"] = ["编码", "名称", "简称", "所属校区编码", "排序", "状态"],
            ["majors"] = ["编码", "名称", "所属学院编码", "学位类型", "学制", "排序", "状态"],
            ["classes"] = ["编码", "名称", "所属专业编码", "年级", "辅导员工号", "排序", "状态"],
            ["terms"] = ["编码", "名称", "学年", "学期季", "开始日期", "结束日期", "当前学期", "状态"],
            ["buildings"] = ["编码", "名称", "所属校区编码", "排序", "状态"],
            ["classrooms"] = ["编码", "名称", "所属教学楼编码", "容量", "教室类型", "教学场地性质", "设备", "排序", "状态"],
            ["course-categories"] = ["编码", "名称", "排序", "状态"]
        };

    [HttpGet("{kind}/template")]
    public IActionResult DownloadTemplate(string kind)
    {
        if (!TryGetHeaders(kind, out var headers)) return NotFound();
        var bytes = ExcelWorkbookHelper.Create(
            "数据导入",
            headers,
            [],
            [
                "请勿修改第一行列名，不要合并单元格。",
                "编码是唯一标识；编码已存在时更新，不存在时新增。",
                "关联字段填写对应数据的编码，请先导入上级基础数据。",
                "状态填写“启用”或“停用”；是/否字段填写“是”或“否”。",
                "整批数据会先校验，任一行有误时均不会写入。"
            ]);
        return File(bytes, ExcelWorkbookHelper.ContentType, $"{KindName(kind)}导入模板.xlsx");
    }

    [HttpGet("{kind}/export")]
    public async Task<IActionResult> Export(string kind, CancellationToken cancellationToken)
    {
        if (!TryGetHeaders(kind, out var headers)) return NotFound();
        var rows = await GetExportRowsAsync(kind, cancellationToken);
        var bytes = ExcelWorkbookHelper.Create(KindName(kind), headers, rows);
        return File(bytes, ExcelWorkbookHelper.ContentType,
            $"{KindName(kind)}-{DateTime.Now:yyyyMMdd-HHmm}.xlsx");
    }

    [HttpPost("{kind}/import")]
    [RequestSizeLimit(10 * 1024 * 1024)]
    public async Task<ActionResult<ExcelImportResult>> Import(
        string kind,
        IFormFile file,
        CancellationToken cancellationToken)
    {
        if (!TryGetHeaders(kind, out var headers)) return NotFound();
        IReadOnlyList<ExcelRow> rows;
        try
        {
            var requiredHeaders = kind.Equals("classrooms", StringComparison.OrdinalIgnoreCase)
                ? headers.Where(x => x != "教学场地性质").ToArray()
                : headers;
            rows = await ExcelWorkbookHelper.ReadAsync(file, requiredHeaders, cancellationToken);
        }
        catch (InvalidDataException exception)
        {
            return ValidationProblem(exception.Message);
        }

        if (rows.Count == 0) return ValidationProblem("Excel 中没有可导入的数据。");
        return await db.ExecuteInRetriableTransactionAsync<
            ActionResult<ExcelImportResult>>(
            async transaction =>
            {
                var errors = new List<string>();
                ExcelImportResult result;
                try
                {
                    result = kind.ToLowerInvariant() switch
                    {
                        "campuses" => await ImportCampusesAsync(
                            rows, errors, cancellationToken),
                        "colleges" => await ImportCollegesAsync(
                            rows, errors, cancellationToken),
                        "majors" => await ImportMajorsAsync(
                            rows, errors, cancellationToken),
                        "classes" => await ImportClassesAsync(
                            rows, errors, cancellationToken),
                        "terms" => await ImportTermsAsync(
                            rows, errors, cancellationToken),
                        "buildings" => await ImportBuildingsAsync(
                            rows, errors, cancellationToken),
                        "classrooms" => await ImportClassroomsAsync(
                            rows, errors, cancellationToken),
                        "course-categories" => await ImportCourseCategoriesAsync(
                            rows, errors, cancellationToken),
                        _ => throw new InvalidOperationException()
                    };

                    if (errors.Count > 0)
                    {
                        await transaction.RollbackAsync(cancellationToken);
                        return ImportValidationProblem(errors);
                    }

                    await db.SaveChangesAsync(cancellationToken);
                    await transaction.CommitAsync(cancellationToken);
                    await cache.RemoveByTagAsync(
                        AppCacheTags.BaseData,
                        cancellationToken);
                    return Ok(result);
                }
                catch (DbUpdateException)
                {
                    await transaction.RollbackAsync(cancellationToken);
                    return Conflict(new ProblemDetails
                    {
                        Title = "导入失败",
                        Detail = "存在重复编码或无效关联，未写入任何数据。",
                        Status = StatusCodes.Status409Conflict
                    });
                }
            },
            cancellationToken);
    }

    private async Task<IReadOnlyList<IReadOnlyList<object?>>> GetExportRowsAsync(
        string kind,
        CancellationToken cancellationToken) =>
        kind.ToLowerInvariant() switch
        {
            "campuses" => (await db.Campuses.AsNoTracking().OrderBy(x => x.Code)
                .ToListAsync(cancellationToken))
                .Select(x => Row(x.Code, x.Name, x.Address, x.SortOrder, Status(x.IsEnabled))).ToList(),
            "colleges" => (await db.Colleges.AsNoTracking().Include(x => x.Campus)
                .OrderBy(x => x.Code).ToListAsync(cancellationToken))
                .Select(x => Row(x.Code, x.Name, x.ShortName, x.Campus?.Code, x.SortOrder,
                    Status(x.IsEnabled))).ToList(),
            "majors" => (await db.Majors.AsNoTracking().Include(x => x.College)
                .OrderBy(x => x.Code).ToListAsync(cancellationToken))
                .Select(x => Row(x.Code, x.Name, x.College!.Code, x.DegreeType,
                    x.SchoolingYears, x.SortOrder, Status(x.IsEnabled))).ToList(),
            "classes" => (await db.AdministrativeClasses.AsNoTracking().Include(x => x.Major)
                .Include(x => x.CounselorUser).OrderBy(x => x.Code).ToListAsync(cancellationToken))
                .Select(x => Row(x.Code, x.Name, x.Major!.Code, x.Grade,
                    x.CounselorUser?.StaffNumber, x.SortOrder, Status(x.IsEnabled))).ToList(),
            "terms" => (await db.AcademicTerms.AsNoTracking().OrderByDescending(x => x.StartDate)
                .ToListAsync(cancellationToken))
                .Select(x => Row(x.Code, x.Name, x.AcademicYear, SeasonName(x.Season),
                    x.StartDate, x.EndDate, YesNo(x.IsCurrent), Status(x.IsEnabled))).ToList(),
            "buildings" => (await db.Buildings.AsNoTracking().Include(x => x.Campus)
                .OrderBy(x => x.Code).ToListAsync(cancellationToken))
                .Select(x => Row(x.Code, x.Name, x.Campus!.Code, x.SortOrder,
                    Status(x.IsEnabled))).ToList(),
            "classrooms" => (await db.Classrooms.AsNoTracking().Include(x => x.Building)
                .OrderBy(x => x.Code).ToListAsync(cancellationToken))
                .Select(x => Row(x.Code, x.Name, x.Building!.Code, x.Capacity, x.RoomType,
                    VenueNatureName(x.TeachingVenueNature),
                    x.Equipment, x.SortOrder, Status(x.IsEnabled))).ToList(),
            "course-categories" => (await db.CourseCategories.AsNoTracking()
                .OrderBy(x => x.SortOrder).ThenBy(x => x.Code)
                .ToListAsync(cancellationToken))
                .Select(x => Row(x.Code, x.Name, x.SortOrder, Status(x.IsEnabled))).ToList(),
            _ => []
        };

    private async Task<ExcelImportResult> ImportCampusesAsync(
        IReadOnlyList<ExcelRow> rows,
        List<string> errors,
        CancellationToken cancellationToken)
    {
        var existing = await db.Campuses.ToDictionaryAsync(x => x.Code,
            StringComparer.OrdinalIgnoreCase, cancellationToken);
        var created = 0;
        var updated = 0;
        foreach (var row in rows)
        {
            var code = Required(row, "编码", errors);
            var name = Required(row, "名称", errors);
            if (code is null || name is null) continue;
            var entity = existing.GetValueOrDefault(code);
            if (entity is null)
            {
                entity = new Campus { Code = code, Name = name };
                db.Campuses.Add(entity);
                existing[code] = entity;
                created++;
            }
            else updated++;
            ApplyCatalog(entity, row, name, errors);
            entity.Address = Optional(row, "地址");
        }
        return new(created, updated, rows.Count);
    }

    private async Task<ExcelImportResult> ImportCollegesAsync(
        IReadOnlyList<ExcelRow> rows,
        List<string> errors,
        CancellationToken cancellationToken)
    {
        var existing = await db.Colleges.ToDictionaryAsync(x => x.Code,
            StringComparer.OrdinalIgnoreCase, cancellationToken);
        var campuses = await db.Campuses.AsNoTracking().ToDictionaryAsync(x => x.Code,
            StringComparer.OrdinalIgnoreCase, cancellationToken);
        var created = 0;
        var updated = 0;
        foreach (var row in rows)
        {
            var code = Required(row, "编码", errors);
            var name = Required(row, "名称", errors);
            if (code is null || name is null) continue;
            Guid? campusId = null;
            var campusCode = Optional(row, "所属校区编码");
            if (campusCode is not null)
            {
                if (!campuses.TryGetValue(campusCode, out var campus))
                    errors.Add($"第 {row.RowNumber} 行：所属校区编码“{campusCode}”不存在。");
                else campusId = campus.Id;
            }
            var entity = existing.GetValueOrDefault(code);
            if (entity is null)
            {
                entity = new College { Code = code, Name = name };
                db.Colleges.Add(entity);
                existing[code] = entity;
                created++;
            }
            else updated++;
            ApplyCatalog(entity, row, name, errors);
            entity.ShortName = Optional(row, "简称");
            entity.CampusId = campusId;
        }
        return new(created, updated, rows.Count);
    }

    private async Task<ExcelImportResult> ImportMajorsAsync(
        IReadOnlyList<ExcelRow> rows,
        List<string> errors,
        CancellationToken cancellationToken)
    {
        var existing = await db.Majors.ToDictionaryAsync(x => x.Code,
            StringComparer.OrdinalIgnoreCase, cancellationToken);
        var colleges = await db.Colleges.AsNoTracking().ToDictionaryAsync(x => x.Code,
            StringComparer.OrdinalIgnoreCase, cancellationToken);
        var created = 0;
        var updated = 0;
        foreach (var row in rows)
        {
            var code = Required(row, "编码", errors);
            var name = Required(row, "名称", errors);
            var collegeCode = Required(row, "所属学院编码", errors);
            var degreeType = Required(row, "学位类型", errors);
            var years = ParseInt(row, "学制", 1, 8, errors);
            if (code is null || name is null || collegeCode is null ||
                degreeType is null || years is null) continue;
            if (!colleges.TryGetValue(collegeCode, out var college))
            {
                errors.Add($"第 {row.RowNumber} 行：所属学院编码“{collegeCode}”不存在。");
                continue;
            }
            var entity = existing.GetValueOrDefault(code);
            if (entity is null)
            {
                entity = new Major
                {
                    Code = code,
                    Name = name,
                    CollegeId = college.Id,
                    DegreeType = degreeType
                };
                db.Majors.Add(entity);
                existing[code] = entity;
                created++;
            }
            else updated++;
            ApplyCatalog(entity, row, name, errors);
            entity.CollegeId = college.Id;
            entity.DegreeType = degreeType;
            entity.SchoolingYears = years.Value;
        }
        return new(created, updated, rows.Count);
    }

    private async Task<ExcelImportResult> ImportClassesAsync(
        IReadOnlyList<ExcelRow> rows,
        List<string> errors,
        CancellationToken cancellationToken)
    {
        var existing = await db.AdministrativeClasses.ToDictionaryAsync(x => x.Code,
            StringComparer.OrdinalIgnoreCase, cancellationToken);
        var majors = await db.Majors.AsNoTracking().ToDictionaryAsync(x => x.Code,
            StringComparer.OrdinalIgnoreCase, cancellationToken);
        var counselors = await db.Users.AsNoTracking()
            .Where(user =>
                user.StaffNumber != null &&
                user.IsEnabled &&
                db.UserRoles.Any(userRole =>
                    userRole.UserId == user.Id &&
                    db.Roles.Any(role =>
                        role.Id == userRole.RoleId &&
                        role.Name == SystemRoles.Counselor)))
            .ToDictionaryAsync(x => x.StaffNumber!, StringComparer.OrdinalIgnoreCase,
                cancellationToken);
        var created = 0;
        var updated = 0;
        foreach (var row in rows)
        {
            var code = Required(row, "编码", errors);
            var name = Required(row, "名称", errors);
            var majorCode = Required(row, "所属专业编码", errors);
            var grade = ParseInt(row, "年级", 2000, 2200, errors);
            if (code is null || name is null || majorCode is null || grade is null) continue;
            if (!majors.TryGetValue(majorCode, out var major))
            {
                errors.Add($"第 {row.RowNumber} 行：所属专业编码“{majorCode}”不存在。");
                continue;
            }
            ApplicationUser? counselor = null;
            var counselorNumber = Optional(row, "辅导员工号");
            if (counselorNumber is not null &&
                !counselors.TryGetValue(counselorNumber, out counselor))
                errors.Add($"第 {row.RowNumber} 行：辅导员工号“{counselorNumber}”不存在或已停用。");
            var entity = existing.GetValueOrDefault(code);
            if (entity is null)
            {
                entity = new AdministrativeClass
                {
                    Code = code,
                    Name = name,
                    MajorId = major.Id,
                    Grade = grade.Value
                };
                db.AdministrativeClasses.Add(entity);
                existing[code] = entity;
                created++;
            }
            else updated++;
            ApplyCatalog(entity, row, name, errors);
            entity.MajorId = major.Id;
            entity.Grade = grade.Value;
            entity.CounselorUserId = counselor?.Id;
            entity.CounselorName = counselor?.DisplayName;
        }
        return new(created, updated, rows.Count);
    }

    private async Task<ExcelImportResult> ImportTermsAsync(
        IReadOnlyList<ExcelRow> rows,
        List<string> errors,
        CancellationToken cancellationToken)
    {
        var existing = await db.AcademicTerms.ToDictionaryAsync(x => x.Code,
            StringComparer.OrdinalIgnoreCase, cancellationToken);
        var created = 0;
        var updated = 0;
        AcademicTerm? requestedCurrent = null;
        foreach (var row in rows)
        {
            var code = Required(row, "编码", errors);
            var name = Required(row, "名称", errors);
            var academicYear = Required(row, "学年", errors);
            var season = ParseSeason(row, errors);
            var startDate = ParseDate(row, "开始日期", true, errors);
            var endDate = ParseDate(row, "结束日期", true, errors);
            if (code is null || name is null || academicYear is null || season is null ||
                startDate is null || endDate is null) continue;
            if (endDate <= startDate)
            {
                errors.Add($"第 {row.RowNumber} 行：结束日期必须晚于开始日期。");
                continue;
            }
            var entity = existing.GetValueOrDefault(code);
            if (entity is null)
            {
                entity = new AcademicTerm
                {
                    Code = code,
                    Name = name,
                    AcademicYear = academicYear,
                    Season = season.Value,
                    StartDate = startDate.Value,
                    EndDate = endDate.Value
                };
                db.AcademicTerms.Add(entity);
                existing[code] = entity;
                created++;
            }
            else updated++;
            entity.Name = name;
            entity.AcademicYear = academicYear;
            entity.Season = season.Value;
            entity.StartDate = startDate.Value;
            entity.EndDate = endDate.Value;
            entity.IsEnabled = ParseEnabled(row, errors);
            entity.IsCurrent = ParseBoolean(row, "当前学期", false, errors);
            if (entity.IsCurrent)
            {
                if (requestedCurrent is not null)
                    errors.Add($"第 {row.RowNumber} 行：同一次导入只能设置一个当前学期。");
                requestedCurrent = entity;
            }
        }
        if (requestedCurrent is not null)
        {
            foreach (var term in await db.AcademicTerms.Where(x => x.Id != requestedCurrent.Id)
                         .ToListAsync(cancellationToken))
                term.IsCurrent = false;
        }
        return new(created, updated, rows.Count);
    }

    private async Task<ExcelImportResult> ImportBuildingsAsync(
        IReadOnlyList<ExcelRow> rows,
        List<string> errors,
        CancellationToken cancellationToken)
    {
        var existing = await db.Buildings.ToDictionaryAsync(x => x.Code,
            StringComparer.OrdinalIgnoreCase, cancellationToken);
        var campuses = await db.Campuses.AsNoTracking().ToDictionaryAsync(x => x.Code,
            StringComparer.OrdinalIgnoreCase, cancellationToken);
        var created = 0;
        var updated = 0;
        foreach (var row in rows)
        {
            var code = Required(row, "编码", errors);
            var name = Required(row, "名称", errors);
            var campusCode = Required(row, "所属校区编码", errors);
            if (code is null || name is null || campusCode is null) continue;
            if (!campuses.TryGetValue(campusCode, out var campus))
            {
                errors.Add($"第 {row.RowNumber} 行：所属校区编码“{campusCode}”不存在。");
                continue;
            }
            var entity = existing.GetValueOrDefault(code);
            if (entity is null)
            {
                entity = new Building { Code = code, Name = name, CampusId = campus.Id };
                db.Buildings.Add(entity);
                existing[code] = entity;
                created++;
            }
            else updated++;
            ApplyCatalog(entity, row, name, errors);
            entity.CampusId = campus.Id;
        }
        return new(created, updated, rows.Count);
    }

    private async Task<ExcelImportResult> ImportClassroomsAsync(
        IReadOnlyList<ExcelRow> rows,
        List<string> errors,
        CancellationToken cancellationToken)
    {
        var existing = await db.Classrooms.ToDictionaryAsync(x => x.Code,
            StringComparer.OrdinalIgnoreCase, cancellationToken);
        var buildings = await db.Buildings.AsNoTracking().ToDictionaryAsync(x => x.Code,
            StringComparer.OrdinalIgnoreCase, cancellationToken);
        var created = 0;
        var updated = 0;
        foreach (var row in rows)
        {
            var code = Required(row, "编码", errors);
            var name = Required(row, "名称", errors);
            var buildingCode = Required(row, "所属教学楼编码", errors);
            var capacity = ParseInt(row, "容量", 1, 1000, errors);
            var roomType = Required(row, "教室类型", errors);
            var venueNature = ParseVenueNature(row, roomType, errors);
            if (code is null || name is null || buildingCode is null ||
                capacity is null || roomType is null || venueNature is null) continue;
            if (!buildings.TryGetValue(buildingCode, out var building))
            {
                errors.Add($"第 {row.RowNumber} 行：所属教学楼编码“{buildingCode}”不存在。");
                continue;
            }
            var entity = existing.GetValueOrDefault(code);
            if (entity is null)
            {
                entity = new Classroom
                {
                    Code = code,
                    Name = name,
                    BuildingId = building.Id,
                    RoomType = roomType,
                    TeachingVenueNature = venueNature.Value
                };
                db.Classrooms.Add(entity);
                existing[code] = entity;
                created++;
            }
            else updated++;
            ApplyCatalog(entity, row, name, errors);
            entity.BuildingId = building.Id;
            entity.Capacity = capacity.Value;
            entity.RoomType = roomType;
            entity.TeachingVenueNature = venueNature.Value;
            entity.Equipment = Optional(row, "设备");
        }
        return new(created, updated, rows.Count);
    }

    private async Task<ExcelImportResult> ImportCourseCategoriesAsync(
        IReadOnlyList<ExcelRow> rows,
        List<string> errors,
        CancellationToken cancellationToken)
    {
        var existing = await db.CourseCategories.ToDictionaryAsync(
            x => x.Code,
            StringComparer.OrdinalIgnoreCase,
            cancellationToken);
        var created = 0;
        var updated = 0;
        foreach (var row in rows)
        {
            var code = Required(row, "编码", errors);
            var name = Required(row, "名称", errors);
            if (code is null || name is null) continue;
            var entity = existing.GetValueOrDefault(code);
            if (entity is null)
            {
                entity = new CourseCategory { Code = code, Name = name };
                db.CourseCategories.Add(entity);
                existing[code] = entity;
                created++;
            }
            else updated++;
            ApplyCatalog(entity, row, name, errors);
        }
        return new(created, updated, rows.Count);
    }

    private ActionResult ImportValidationProblem(IReadOnlyList<string> errors)
    {
        foreach (var error in errors.Take(50))
            ModelState.AddModelError("file", error);
        if (errors.Count > 50)
            ModelState.AddModelError("file", $"另有 {errors.Count - 50} 条错误未显示。");
        return ValidationProblem(ModelState);
    }

    private static void ApplyCatalog(
        CatalogEntity entity,
        ExcelRow row,
        string name,
        List<string> errors)
    {
        entity.Name = name;
        entity.SortOrder = string.IsNullOrWhiteSpace(row["排序"])
            ? 0
            : ParseInt(row, "排序", 0, int.MaxValue, errors) ?? 0;
        entity.IsEnabled = ParseEnabled(row, errors);
    }

    private static string? Required(ExcelRow row, string header, List<string> errors)
    {
        var value = Optional(row, header);
        if (value is not null) return value;
        errors.Add($"第 {row.RowNumber} 行：“{header}”不能为空。");
        return null;
    }

    private static string? Optional(ExcelRow row, string header) =>
        string.IsNullOrWhiteSpace(row[header]) ? null : row[header].Trim();

    private static int? ParseInt(
        ExcelRow row, string header, int minimum, int maximum, List<string> errors)
    {
        if (int.TryParse(row[header], out var value) && value >= minimum && value <= maximum)
            return value;
        errors.Add($"第 {row.RowNumber} 行：“{header}”必须是 {minimum} 至 {maximum} 的整数。");
        return null;
    }

    private static DateOnly? ParseDate(
        ExcelRow row, string header, bool required, List<string> errors)
    {
        var text = row[header];
        if (string.IsNullOrWhiteSpace(text) && !required) return null;
        if (DateOnly.TryParse(text, out var value)) return value;
        errors.Add($"第 {row.RowNumber} 行：“{header}”必须是有效日期（如 2026-09-01）。");
        return null;
    }

    private static bool ParseEnabled(ExcelRow row, List<string> errors)
    {
        var value = row["状态"];
        if (value is "启用" or "是" or "1" ||
            value.Equals("true", StringComparison.OrdinalIgnoreCase)) return true;
        if (value is "停用" or "否" or "0" ||
            value.Equals("false", StringComparison.OrdinalIgnoreCase)) return false;
        errors.Add($"第 {row.RowNumber} 行：“状态”请填写“启用”或“停用”。");
        return true;
    }

    private static TeachingVenueNature? ParseVenueNature(
        ExcelRow row,
        string? roomType,
        List<string> errors)
    {
        var value = Optional(row, "教学场地性质");
        if (value is null) return InferVenueNature(roomType ?? string.Empty);
        var result = (TeachingVenueNature)0;
        foreach (var part in value.Split(['、', '，', ',', ';', '；'],
                     StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries))
        {
            result |= part switch
            {
                "普通教室" => TeachingVenueNature.GeneralClassroom,
                "实验室" => TeachingVenueNature.Laboratory,
                "实训室" => TeachingVenueNature.TrainingRoom,
                "计算机机房" or "机房" => TeachingVenueNature.ComputerLab,
                "语音室" => TeachingVenueNature.LanguageLab,
                "体育场地" => TeachingVenueNature.SportsVenue,
                "艺术场地" => TeachingVenueNature.ArtsVenue,
                _ => (TeachingVenueNature)0
            };
            if (part is not ("普通教室" or "实验室" or "实训室" or "计算机机房" or "机房" or "语音室" or "体育场地" or "艺术场地"))
                errors.Add($"第 {row.RowNumber} 行：“教学场地性质”包含不支持的值“{part}”。");
        }
        return result == 0 ? null : result;
    }

    private static TeachingVenueNature InferVenueNature(string roomType) =>
        roomType.Contains("机房", StringComparison.OrdinalIgnoreCase)
            ? TeachingVenueNature.Laboratory | TeachingVenueNature.ComputerLab
            : roomType.Contains("语音", StringComparison.OrdinalIgnoreCase)
                ? TeachingVenueNature.Laboratory | TeachingVenueNature.LanguageLab
                : roomType.Contains("实训", StringComparison.OrdinalIgnoreCase)
                    ? TeachingVenueNature.TrainingRoom
                    : roomType.Contains("实验", StringComparison.OrdinalIgnoreCase)
                        ? TeachingVenueNature.Laboratory
                        : TeachingVenueNature.GeneralClassroom;

    private static string VenueNatureName(TeachingVenueNature value) => string.Join("、",
        new[]
        {
            (TeachingVenueNature.GeneralClassroom, "普通教室"),
            (TeachingVenueNature.Laboratory, "实验室"),
            (TeachingVenueNature.TrainingRoom, "实训室"),
            (TeachingVenueNature.ComputerLab, "计算机机房"),
            (TeachingVenueNature.LanguageLab, "语音室"),
            (TeachingVenueNature.SportsVenue, "体育场地"),
            (TeachingVenueNature.ArtsVenue, "艺术场地")
        }.Where(x => (value & x.Item1) != 0).Select(x => x.Item2));

    private static bool ParseBoolean(
        ExcelRow row, string header, bool defaultValue, List<string> errors)
    {
        var value = row[header];
        if (string.IsNullOrWhiteSpace(value)) return defaultValue;
        if (value is "是" or "1" || value.Equals("true", StringComparison.OrdinalIgnoreCase))
            return true;
        if (value is "否" or "0" || value.Equals("false", StringComparison.OrdinalIgnoreCase))
            return false;
        errors.Add($"第 {row.RowNumber} 行：“{header}”请填写“是”或“否”。");
        return defaultValue;
    }

    private static TermSeason? ParseSeason(ExcelRow row, List<string> errors)
    {
        var value = row["学期季"];
        if (value is "秋季" or "秋季学期" ||
            value.Equals("Autumn", StringComparison.OrdinalIgnoreCase)) return TermSeason.Autumn;
        if (value is "春季" or "春季学期" ||
            value.Equals("Spring", StringComparison.OrdinalIgnoreCase)) return TermSeason.Spring;
        if (value is "夏季" or "夏季学期" ||
            value.Equals("Summer", StringComparison.OrdinalIgnoreCase)) return TermSeason.Summer;
        errors.Add($"第 {row.RowNumber} 行：“学期季”请填写秋季、春季或夏季。");
        return null;
    }

    private static bool TryGetHeaders(string kind, out string[] headers) =>
        Headers.TryGetValue(kind, out headers!);

    private static IReadOnlyList<object?> Row(params object?[] values) => values;
    private static string Status(bool enabled) => enabled ? "启用" : "停用";
    private static string YesNo(bool value) => value ? "是" : "否";
    private static string SeasonName(TermSeason season) => season switch
    {
        TermSeason.Autumn => "秋季",
        TermSeason.Spring => "春季",
        _ => "夏季"
    };

    private static string KindName(string kind) => kind.ToLowerInvariant() switch
    {
        "campuses" => "校区",
        "colleges" => "学院",
        "majors" => "专业",
        "classes" => "行政班",
        "terms" => "学期",
        "buildings" => "教学楼",
        "classrooms" => "教室",
        "course-categories" => "课程分类",
        _ => "基础数据"
    };
}
