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

namespace Jiaowu.Api.Controllers;

[ApiController]
[Authorize(Roles = ManagementRoles)]
[Route("api/timetables/management")]
public sealed class TimetableManagementController(
    AppDbContext db,
    TimetableDataService timetableDataService,
    ICurrentUserDataScope currentUserDataScope,
    OfficialDocumentOptions officialDocumentOptions) : ControllerBase
{
    private const string ManagementRoles =
        SystemRoles.SuperAdmin + "," +
        SystemRoles.AcademicAdmin + "," +
        SystemRoles.CollegeAdmin + "," +
        SystemRoles.Leader;

    [HttpGet("options")]
    public async Task<ActionResult> GetOptions(
        Guid academicTermId,
        CancellationToken cancellationToken)
    {
        var scope = currentUserDataScope.Current;
        var collegeId = scope.RestrictedCollegeId;
        var classes = db.AdministrativeClasses.AsNoTracking()
            .Where(x => x.IsEnabled && x.Major!.IsEnabled && x.Major.College!.IsEnabled);
        var teachers = db.Teachers.AsNoTracking()
            .Where(x => x.Status == TeacherStatus.Active && x.College!.IsEnabled);
        var colleges = db.Colleges.AsNoTracking().Where(x => x.IsEnabled);
        var majors = db.Majors.AsNoTracking().Where(x => x.IsEnabled);
        if (collegeId.HasValue)
        {
            classes = classes.Where(x => x.Major!.CollegeId == collegeId.Value);
            teachers = teachers.Where(x => x.CollegeId == collegeId.Value);
            colleges = colleges.Where(x => x.Id == collegeId.Value);
            majors = majors.Where(x => x.CollegeId == collegeId.Value);
        }

        var plans = await db.SchedulePlans.AsNoTracking()
            .Where(x =>
                x.AcademicTermId == academicTermId &&
                x.Status != SchedulePlanStatus.Archived)
            .OrderByDescending(x => x.Status == SchedulePlanStatus.Published)
            .ThenByDescending(x => x.UpdatedAt)
            .Select(x => new
            {
                x.Id,
                x.Name,
                x.Version,
                x.Status,
                x.PublishedAt,
                x.UpdatedAt,
                EntryCount = x.Entries.Count
            })
            .ToListAsync(cancellationToken);
        var classOptions = await classes
            .OrderByDescending(x => x.Grade)
            .ThenBy(x => x.Code)
            .Select(x => new
            {
                x.Id,
                x.Code,
                x.Name,
                x.Grade,
                x.MajorId,
                MajorName = x.Major!.Name,
                CollegeId = x.Major.CollegeId,
                CollegeName = x.Major.College!.Name
            })
            .ToListAsync(cancellationToken);
        var teacherOptions = await teachers
            .OrderBy(x => x.TeacherNumber)
            .Select(x => new
            {
                x.Id,
                x.TeacherNumber,
                x.Name,
                x.Title,
                x.CollegeId,
                CollegeName = x.College!.Name
            })
            .ToListAsync(cancellationToken);
        var campusOptions = await db.Campuses.AsNoTracking()
            .Where(x => x.IsEnabled)
            .OrderBy(x => x.SortOrder)
            .Select(x => new { x.Id, x.Code, x.Name })
            .ToListAsync(cancellationToken);
        var buildingOptions = await db.Buildings.AsNoTracking()
            .Where(x => x.IsEnabled && x.Campus!.IsEnabled)
            .OrderBy(x => x.SortOrder)
            .Select(x => new { x.Id, x.Code, x.Name, x.CampusId })
            .ToListAsync(cancellationToken);
        var classroomOptions = await db.Classrooms.AsNoTracking()
            .Where(x => x.IsEnabled && x.Building!.IsEnabled && x.Building.Campus!.IsEnabled)
            .OrderBy(x => x.Building!.Code)
            .ThenBy(x => x.Code)
            .Select(x => new
            {
                x.Id,
                x.Code,
                x.Name,
                x.Capacity,
                x.BuildingId,
                BuildingName = x.Building!.Name,
                CampusId = x.Building.CampusId,
                CampusName = x.Building.Campus!.Name
            })
            .ToListAsync(cancellationToken);
        return Ok(new
        {
            Plans = plans,
            Colleges = await colleges.OrderBy(x => x.Code)
                .Select(x => new { x.Id, x.Code, x.Name })
                .ToListAsync(cancellationToken),
            Majors = await majors.OrderBy(x => x.Code)
                .Select(x => new { x.Id, x.Code, x.Name, x.CollegeId })
                .ToListAsync(cancellationToken),
            Classes = classOptions,
            Teachers = teacherOptions,
            Campuses = campusOptions,
            Buildings = buildingOptions,
            Classrooms = classroomOptions
        });
    }

    [HttpGet("query")]
    public async Task<ActionResult> Query(
        TimetableResourceType resourceType,
        Guid resourceId,
        Guid academicTermId,
        Guid? schedulePlanId,
        CancellationToken cancellationToken)
    {
        var result = await LoadAuthorizedAsync(
            resourceType,
            resourceId,
            academicTermId,
            schedulePlanId,
            cancellationToken);
        return result.Result is null ? result.Error! : Ok(result.Result);
    }

    [HttpGet("export.xlsx")]
    public async Task<ActionResult> Export(
        TimetableResourceType resourceType,
        Guid resourceId,
        Guid academicTermId,
        Guid? schedulePlanId,
        CancellationToken cancellationToken)
    {
        var result = await LoadAuthorizedAsync(
            resourceType,
            resourceId,
            academicTermId,
            schedulePlanId,
            cancellationToken);
        if (result.Result is null) return result.Error!;
        var bytes = TimetableExcelExporter.Create(result.Result);
        var fileName =
            $"{FileName(result.Result.Subject.Name)}-{FileName(result.Result.Term.Name)}-课表.xlsx";
        return File(bytes, ExcelWorkbookHelper.ContentType, fileName);
    }

    [HttpPost("export/batch.xlsx")]
    public async Task<ActionResult> ExportBatch(
        TimetableBatchExportRequest request,
        CancellationToken cancellationToken)
    {
        var resourceIds = request.ResourceIds.Distinct().ToArray();
        if (resourceIds.Length is 0 or > 100)
            return ValidationProblem("请选择 1 至 100 个课表对象进行导出。", statusCode: StatusCodes.Status400BadRequest);

        var timetables = new List<TimetableData>(resourceIds.Length);
        foreach (var resourceId in resourceIds)
        {
            var result = await LoadAuthorizedAsync(
                request.ResourceType,
                resourceId,
                request.AcademicTermId,
                request.SchedulePlanId,
                cancellationToken);
            if (result.Result is null) return result.Error!;
            timetables.Add(result.Result);
        }

        var bytes = TimetableExcelExporter.Create(timetables);
        var fileName = $"课表批量导出-{FileName(timetables[0].Term.Name)}.xlsx";
        return File(bytes, ExcelWorkbookHelper.ContentType, fileName);
    }

    [HttpGet("export.pdf")]
    public async Task<ActionResult> ExportPdf(
        TimetableResourceType resourceType,
        Guid resourceId,
        Guid academicTermId,
        Guid? schedulePlanId,
        CancellationToken cancellationToken)
    {
        var result = await LoadAuthorizedAsync(
            resourceType,
            resourceId,
            academicTermId,
            schedulePlanId,
            cancellationToken);
        if (result.Result is null) return result.Error!;
        return PdfFile(result.Result);
    }

    [HttpPost("export/batch.pdf")]
    public async Task<ActionResult> ExportBatchPdf(
        TimetableBatchExportRequest request,
        CancellationToken cancellationToken)
    {
        var resourceIds = request.ResourceIds.Distinct().ToArray();
        if (resourceIds.Length is 0 or > 100)
            return ValidationProblem("请选择 1 至 100 个课表对象进行导出。", statusCode: StatusCodes.Status400BadRequest);

        var timetables = new List<TimetableData>(resourceIds.Length);
        foreach (var resourceId in resourceIds)
        {
            var result = await LoadAuthorizedAsync(
                request.ResourceType,
                resourceId,
                request.AcademicTermId,
                request.SchedulePlanId,
                cancellationToken);
            if (result.Result is null) return result.Error!;
            timetables.Add(result.Result);
        }

        var fileName = $"课表批量导出-{FileName(timetables[0].Term.Name)}.pdf";
        return File(TimetablePdfExporter.Create(timetables, officialDocumentOptions.FontPath), "application/pdf", fileName);
    }

    [HttpPost("display-links/export.xlsx")]
    public async Task<ActionResult> ExportDisplayLinks(VenueDisplayLinkExportRequest request, CancellationToken cancellationToken)
    {
        var classroomIds = request.ClassroomIds.Distinct().Take(200).ToArray();
        var buildingIds = request.BuildingIds.Distinct().Take(100).ToArray();
        if (classroomIds.Length + buildingIds.Length == 0)
            return ValidationProblem("请至少选择一个教室或教学楼。", statusCode: StatusCodes.Status400BadRequest);
        var classrooms = await db.Classrooms.AsNoTracking().WhereIn(classroomIds, x => x.Id)
            .OrderBy(x => x.Building!.Name).ThenBy(x => x.Code)
            .Select(x => new object?[] { "教室当天课程", x.Building!.Name + " · " + x.Name, "/venue-display/classroom/" + x.Id })
            .ToListAsync(cancellationToken);
        var buildings = await db.Buildings.AsNoTracking().WhereIn(buildingIds, x => x.Id).OrderBy(x => x.Name)
            .Select(x => new object?[] { "教学楼空余教室", x.Name, "/venue-display/building/" + x.Id })
            .ToListAsync(cancellationToken);
        var bytes = ExcelWorkbookHelper.Create("展牌链接", ["展牌类型", "场地", "相对链接"], classrooms.Concat(buildings).ToList());
        return File(bytes, ExcelWorkbookHelper.ContentType, "场地信息展牌链接.xlsx");
    }

    private async Task<(TimetableData? Result, ActionResult? Error)> LoadAuthorizedAsync(
        TimetableResourceType resourceType,
        Guid resourceId,
        Guid academicTermId,
        Guid? schedulePlanId,
        CancellationToken cancellationToken)
    {
        if (!Enum.IsDefined(resourceType))
            return (null, ValidationProblem("课表类型无效。"));
        var result = await timetableDataService.BuildAsync(
            resourceType,
            resourceId,
            academicTermId,
            schedulePlanId,
            true,
            null,
            null,
            cancellationToken);
        if (result is null || schedulePlanId.HasValue && result.Plan is null)
            return (null, NotFound());
        var scope = currentUserDataScope.Current;
        if (resourceType is TimetableResourceType.Class or TimetableResourceType.Teacher &&
            result.Subject.CollegeId.HasValue &&
            !scope.CanAccessCollege(result.Subject.CollegeId.Value))
            return (null, Forbid());
        return (result, null);
    }

    private static string FileName(string value)
    {
        foreach (var character in Path.GetInvalidFileNameChars())
            value = value.Replace(character, '-');
        return value.Trim();
    }

    private ActionResult PdfFile(TimetableData timetable) =>
        File(
            TimetablePdfExporter.Create(timetable, officialDocumentOptions.FontPath),
            "application/pdf",
            $"{FileName(timetable.Subject.Name)}-{FileName(timetable.Term.Name)}-课表.pdf");
}

public sealed record TimetableBatchExportRequest(
    TimetableResourceType ResourceType,
    [param: Required, MinLength(1), MaxLength(100)] IReadOnlyCollection<Guid> ResourceIds,
    Guid AcademicTermId,
    Guid? SchedulePlanId);

public sealed record VenueDisplayLinkExportRequest(IReadOnlyCollection<Guid> ClassroomIds, IReadOnlyCollection<Guid> BuildingIds);

[ApiController]
[Route("api/timetables")]
public sealed class FreeClassroomsController(
    AppDbContext db,
    ClassroomReservationAvailabilityService reservationAvailability) : ControllerBase
{
    [HttpGet("free-classrooms/options")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> GetOptions(
        Guid? academicTermId,
        CancellationToken cancellationToken)
    {
        var terms = await db.AcademicTerms.AsNoTracking()
            .Where(x => x.IsEnabled)
            .OrderByDescending(x => x.IsCurrent)
            .ThenBy(x => x.IsArchived)
            .ThenByDescending(x => x.StartDate)
            .Select(x => new
            {
                x.Id,
                x.Name,
                x.StartDate,
                x.EndDate,
                x.IsCurrent,
                x.IsArchived,
                HasPublishedTimetable = db.SchedulePlans.Any(plan =>
                    plan.AcademicTermId == x.Id &&
                    plan.Status == SchedulePlanStatus.Published)
            })
            .ToListAsync(cancellationToken);
        var selectedTermId = academicTermId
            ?? terms.FirstOrDefault(x => x.IsCurrent && x.HasPublishedTimetable)?.Id
            ?? terms.FirstOrDefault(x => !x.IsArchived && x.HasPublishedTimetable)?.Id
            ?? terms.FirstOrDefault(x => x.HasPublishedTimetable)?.Id;
        var campuses = await db.Campuses.AsNoTracking()
            .Where(x => x.IsEnabled)
            .OrderBy(x => x.SortOrder)
            .ThenBy(x => x.Code)
            .Select(x => new { x.Id, x.Code, x.Name })
            .ToListAsync(cancellationToken);
        var buildings = await db.Buildings.AsNoTracking()
            .Where(x => x.IsEnabled && x.Campus!.IsEnabled)
            .OrderBy(x => x.Campus!.SortOrder)
            .ThenBy(x => x.SortOrder)
            .ThenBy(x => x.Code)
            .Select(x => new { x.Id, x.Code, x.Name, x.CampusId })
            .ToListAsync(cancellationToken);
        var timeSlots = selectedTermId.HasValue
            ? await db.ScheduleTimeSlots.AsNoTracking()
                .Where(x => x.AcademicTermId == selectedTermId.Value && x.IsEnabled)
                .OrderBy(x => x.PeriodNumber)
                .Select(x => new FreeClassroomTimeSlotDto(
                    x.PeriodNumber,
                    x.Name,
                    x.StartsAt.ToString("HH:mm"),
                    x.EndsAt.ToString("HH:mm")))
                .ToListAsync(cancellationToken)
            : [];
        if (timeSlots.Count == 0)
        {
            timeSlots = Enumerable.Range(1, 12)
                .Select(period => new FreeClassroomTimeSlotDto(
                    period,
                    $"第 {period} 节",
                    "",
                    ""))
                .ToList();
        }
        return Ok(new
        {
            Terms = terms,
            SelectedTermId = selectedTermId,
            Campuses = campuses,
            Buildings = buildings,
            TimeSlots = timeSlots
        });
    }

    [HttpGet("free-classrooms")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> Get(
        Guid academicTermId,
        [Range(1, 30)] int week,
        [Range(1, 7)] int dayOfWeek,
        [Range(1, 30)] int startPeriod,
        [Range(1, 6)] int periodCount = 1,
        Guid? campusId = null,
        Guid? buildingId = null,
        [Range(0, 10000)] int? minimumCapacity = null,
        CancellationToken cancellationToken = default)
    {
        var term = await db.AcademicTerms.AsNoTracking()
            .FirstOrDefaultAsync(
                x => x.Id == academicTermId && x.IsEnabled,
                cancellationToken);
        if (term is null) return NotFound();
        var plan = await db.SchedulePlans.AsNoTracking()
            .Where(x =>
                x.AcademicTermId == academicTermId &&
                x.Status == SchedulePlanStatus.Published)
            .Select(x => new { x.Id, x.Version })
            .FirstOrDefaultAsync(cancellationToken);
        if (plan is null)
            return Conflict(new ProblemDetails
            {
                Title = "课表尚未发布",
                Detail = "所选学期尚未发布正式课表，暂时无法准确查询空闲教室。",
                Status = StatusCodes.Status409Conflict
            });

        var requestedPeriods = Enumerable.Range(startPeriod, periodCount).ToArray();
        var configuredPeriodCount = await db.ScheduleTimeSlots.AsNoTracking()
            .CountAsync(x => x.AcademicTermId == academicTermId, cancellationToken);
        var activePeriodCount = configuredPeriodCount == 0
            ? requestedPeriods.Length
            : await db.ScheduleTimeSlots.AsNoTracking()
                .Where(x =>
                    x.AcademicTermId == academicTermId &&
                    x.IsEnabled)
                .WhereIn(requestedPeriods, x => x.PeriodNumber)
                .CountAsync(cancellationToken);
        if (activePeriodCount != requestedPeriods.Length)
            return ValidationProblem("查询范围包含不存在或未启用的节次。");

        var reservationDate = ResolveReservationDate(term, week, dayOfWeek);
        if (reservationDate < term.StartDate || reservationDate > term.EndDate)
            return ValidationProblem("所选周次和星期不在学期日期范围内。");
        var occupiedIds =
            await reservationAvailability.GetOccupiedClassroomIdsAsync(
                term,
                reservationDate,
                startPeriod,
                periodCount,
                null,
                cancellationToken);

        var rooms = db.Classrooms.AsNoTracking()
            .Where(x =>
                x.IsEnabled &&
                x.Building!.IsEnabled &&
                x.Building.Campus!.IsEnabled)
            .WhereNotIn(occupiedIds, x => x.Id);
        if (campusId.HasValue)
            rooms = rooms.Where(x => x.Building!.CampusId == campusId.Value);
        if (buildingId.HasValue)
            rooms = rooms.Where(x => x.BuildingId == buildingId.Value);
        if (minimumCapacity.HasValue)
            rooms = rooms.Where(x => x.Capacity >= minimumCapacity.Value);
        var result = await rooms
            .OrderBy(x => x.Building!.Campus!.SortOrder)
            .ThenBy(x => x.Building!.SortOrder)
            .ThenBy(x => x.Code)
            .Select(x => new
            {
                x.Id,
                x.Code,
                x.Name,
                x.Capacity,
                x.BuildingId,
                BuildingName = x.Building!.Name,
                CampusId = x.Building.CampusId,
                CampusName = x.Building.Campus!.Name
            })
            .ToListAsync(cancellationToken);
        return Ok(new
        {
            Term = term,
            Plan = plan,
            Week = week,
            DayOfWeek = dayOfWeek,
            StartPeriod = startPeriod,
            PeriodCount = periodCount,
            Items = result
        });
    }

    private static DateOnly ResolveReservationDate(
        AcademicTerm term,
        int week,
        int dayOfWeek)
    {
        var startDay = (int)term.StartDate.DayOfWeek;
        var daysSinceMonday = (startDay + 6) % 7;
        var firstWeekMonday = term.StartDate.AddDays(-daysSinceMonday);
        return firstWeekMonday.AddDays((week - 1) * 7 + dayOfWeek - 1);
    }

}

public sealed record FreeClassroomTimeSlotDto(
    int PeriodNumber,
    string Name,
    string StartTime,
    string EndTime);
