using System.Security.Claims;
using System.Security.Cryptography;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Caching;
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]
[Route("api/timetables")]
public sealed class TimetablesController(
    AppDbContext db,
    TimetableDataService timetableDataService,
    PersonalCalendarService personalCalendarService,
    IAppCache cache,
    OfficialDocumentOptions officialDocumentOptions) : ControllerBase
{
    [HttpGet("options")]
    [AllowAnonymous]
    public async Task<ActionResult> GetOptions(CancellationToken cancellationToken)
    {
        var result = await cache.GetOrCreateAsync(
            AppCacheKeys.TimetableOptions,
            LoadOptionsAsync,
            AppCacheProfile.ReferenceData,
            [
                AppCacheTags.BaseData,
                AppCacheTags.Timetables,
                AppCacheTags.TimetableOptions
            ],
            cancellationToken);
        return Ok(result);
    }

    [HttpGet("classes/{classId:guid}")]
    [AllowAnonymous]
    public Task<ActionResult> GetClassTimetable(
        Guid classId,
        Guid? academicTermId,
        CancellationToken cancellationToken) =>
         BuildTimetableAsync(classId, academicTermId, null, cancellationToken);

    [HttpGet("my-class")]
    [Authorize(Roles = SystemRoles.Student)]
    public async Task<ActionResult> GetMyClass(CancellationToken cancellationToken)
    {
        if (!Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId))
            return Unauthorized();

        var administrativeClassId = await db.Students.AsNoTracking()
            .Where(x => x.UserId == userId)
            .Select(x => (Guid?)x.AdministrativeClassId)
            .FirstOrDefaultAsync(cancellationToken);
        if (!administrativeClassId.HasValue)
        {
            return Conflict(new ProblemDetails
            {
                Title = "档案未关联",
                Detail = "当前登录账号没有关联学生档案，请联系教务管理员。",
                Status = StatusCodes.Status409Conflict
            });
        }

        return Ok(new { AdministrativeClassId = administrativeClassId.Value });
    }

    [HttpGet("teachers/{teacherId:guid}")]
    [AllowAnonymous]
    public async Task<ActionResult> GetTeacherTimetable(
        Guid teacherId,
        Guid? academicTermId,
        CancellationToken cancellationToken)
    {
        var result = await GetPublishedTimetableAsync(
            TimetableResourceType.Teacher,
            teacherId,
            academicTermId,
            cancellationToken);
        return result is null ? NotFound() : Ok(result);
    }

    [HttpGet("teachers/{teacherId:guid}/export.xlsx")]
    [AllowAnonymous]
    public async Task<ActionResult> ExportTeacherTimetable(
        Guid teacherId,
        Guid? academicTermId,
        CancellationToken cancellationToken)
    {
        var result = await GetPublishedTimetableAsync(
            TimetableResourceType.Teacher,
            teacherId,
            academicTermId,
            cancellationToken);
        if (result is null) return NotFound();
        return ExcelFile(result);
    }

    [HttpGet("teachers/{teacherId:guid}/export.pdf")]
    [AllowAnonymous]
    public async Task<ActionResult> ExportTeacherTimetablePdf(
        Guid teacherId,
        Guid? academicTermId,
        CancellationToken cancellationToken)
    {
        var result = await GetPublishedTimetableAsync(
            TimetableResourceType.Teacher,
            teacherId,
            academicTermId,
            cancellationToken);
        return result is null ? NotFound() : PdfFile(result);
    }

    [HttpGet("classes/{classId:guid}/export.xlsx")]
    [AllowAnonymous]
    public async Task<ActionResult> ExportClassTimetable(
        Guid classId,
        Guid? academicTermId,
        CancellationToken cancellationToken)
    {
        var result = await GetPublishedTimetableAsync(
            TimetableResourceType.Class,
            classId,
            academicTermId,
            cancellationToken);
        if (result is null) return NotFound();
        return ExcelFile(result);
    }

    [HttpGet("classes/{classId:guid}/export.pdf")]
    [AllowAnonymous]
    public async Task<ActionResult> ExportClassTimetablePdf(
        Guid classId,
        Guid? academicTermId,
        CancellationToken cancellationToken)
    {
        var result = await GetPublishedTimetableAsync(
            TimetableResourceType.Class,
            classId,
            academicTermId,
            cancellationToken);
        return result is null ? NotFound() : PdfFile(result);
    }

    [HttpGet("mine")]
    [Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
    public async Task<ActionResult> GetMyTimetable(
        Guid? academicTermId,
        CancellationToken cancellationToken)
    {
        if (!Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId))
            return Unauthorized();

        var student = await db.Students.AsNoTracking()
            .Where(x => x.UserId == userId)
            .Select(x => new
            {
                x.Id,
                x.AdministrativeClassId,
                x.StudentNumber,
                x.Name
            })
            .FirstOrDefaultAsync(cancellationToken);
        if (student is not null)
        {
            return await BuildTimetableAsync(
                 student.AdministrativeClassId,
                 academicTermId,
                 student.Id,
                 cancellationToken,
                 new TimetableStudentDto(student.StudentNumber, student.Name));
        }

        var teacher = await db.Teachers.AsNoTracking()
            .Where(x => x.UserId == userId && x.Status == TeacherStatus.Active)
            .Select(x => new { x.Id })
            .FirstOrDefaultAsync(cancellationToken);
        if (teacher is not null)
        {
            var result = await timetableDataService.BuildAsync(
                TimetableResourceType.Teacher,
                teacher.Id,
                academicTermId,
                null,
                false,
                null,
                null,
                cancellationToken);
            return result is null ? NotFound() : Ok(result);
        }

        return Conflict(new ProblemDetails
        {
            Title = "档案未关联",
            Detail = "当前登录账号没有关联教师或学生档案，请联系教务管理员。",
            Status = StatusCodes.Status409Conflict
        });
    }

    [HttpGet("mine/export.xlsx")]
    [Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
    public async Task<ActionResult> ExportMyTimetable(
        Guid? academicTermId,
        CancellationToken cancellationToken)
    {
        if (!Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId))
            return Unauthorized();

        var student = await db.Students.AsNoTracking()
            .Where(x => x.UserId == userId)
            .Select(x => new
            {
                x.Id,
                x.AdministrativeClassId,
                x.StudentNumber,
                x.Name
            })
            .FirstOrDefaultAsync(cancellationToken);
        if (student is not null)
        {
            var result = await timetableDataService.BuildAsync(
                TimetableResourceType.Class,
                student.AdministrativeClassId,
                academicTermId,
                null,
                false,
                student.Id,
                new TimetableStudentDto(student.StudentNumber, student.Name),
                cancellationToken);
            if (result is null) return NotFound();
            return ExcelFile(result);
        }

        var teacher = await db.Teachers.AsNoTracking()
            .Where(x => x.UserId == userId && x.Status == TeacherStatus.Active)
            .Select(x => new { x.Id })
            .FirstOrDefaultAsync(cancellationToken);
        if (teacher is not null)
        {
            var result = await timetableDataService.BuildAsync(
                TimetableResourceType.Teacher,
                teacher.Id,
                academicTermId,
                null,
                false,
                null,
                null,
                cancellationToken);
            if (result is null) return NotFound();
            return ExcelFile(result);
        }

        return NotFound();
    }

    [HttpGet("mine/export.pdf")]
    [Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
    public async Task<ActionResult> ExportMyTimetablePdf(
        Guid? academicTermId,
        CancellationToken cancellationToken)
    {
        if (!Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId))
            return Unauthorized();

        var student = await db.Students.AsNoTracking()
            .Where(x => x.UserId == userId)
            .Select(x => new { x.Id, x.AdministrativeClassId, x.StudentNumber, x.Name })
            .FirstOrDefaultAsync(cancellationToken);
        if (student is not null)
        {
            var result = await timetableDataService.BuildAsync(
                TimetableResourceType.Class, student.AdministrativeClassId, academicTermId, null,
                false, student.Id, new TimetableStudentDto(student.StudentNumber, student.Name),
                cancellationToken);
            return result is null ? NotFound() : PdfFile(result);
        }

        var teacher = await db.Teachers.AsNoTracking()
            .Where(x => x.UserId == userId && x.Status == TeacherStatus.Active)
            .Select(x => new { x.Id })
            .FirstOrDefaultAsync(cancellationToken);
        if (teacher is null) return NotFound();
        var teacherResult = await timetableDataService.BuildAsync(
            TimetableResourceType.Teacher, teacher.Id, academicTermId, null, false, null, null,
            cancellationToken);
        return teacherResult is null ? NotFound() : PdfFile(teacherResult);
    }

    [HttpGet("mine/calendar-subscription")]
    [Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
    public async Task<ActionResult> GetMyCalendarSubscription(
        CancellationToken cancellationToken)
    {
        var user = await CurrentUserAsync(cancellationToken);
        if (user is null) return Unauthorized();
        return Ok(BuildCalendarSubscriptionResponse(user));
    }

    [HttpPost("mine/calendar-subscription")]
    [Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
    public async Task<ActionResult> EnableMyCalendarSubscription(
        CancellationToken cancellationToken)
    {
        var user = await CurrentUserAsync(cancellationToken);
        if (user is null) return Unauthorized();
        if (string.IsNullOrWhiteSpace(user.CalendarSubscriptionStamp))
        {
            user.CalendarSubscriptionStamp = PersonalCalendarService.CreateStamp();
            user.CalendarSubscriptionCreatedAt = DateTime.UtcNow;
            await db.SaveChangesAsync(cancellationToken);
        }
        return Ok(BuildCalendarSubscriptionResponse(user));
    }

    [HttpPost("mine/calendar-subscription/rotate")]
    [Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
    public async Task<ActionResult> RotateMyCalendarSubscription(
        CancellationToken cancellationToken)
    {
        var user = await CurrentUserAsync(cancellationToken);
        if (user is null) return Unauthorized();
        user.CalendarSubscriptionStamp = PersonalCalendarService.CreateStamp();
        user.CalendarSubscriptionCreatedAt = DateTime.UtcNow;
        await db.SaveChangesAsync(cancellationToken);
        return Ok(BuildCalendarSubscriptionResponse(user));
    }

    [HttpDelete("mine/calendar-subscription")]
    [Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
    public async Task<ActionResult> DisableMyCalendarSubscription(
        CancellationToken cancellationToken)
    {
        var user = await CurrentUserAsync(cancellationToken);
        if (user is null) return Unauthorized();
        user.CalendarSubscriptionStamp = null;
        user.CalendarSubscriptionCreatedAt = null;
        await db.SaveChangesAsync(cancellationToken);
        return NoContent();
    }

    [HttpGet("mine/calendar.ics")]
    [Authorize(Roles = SystemRoles.Student + "," + SystemRoles.Teacher)]
    public async Task<ActionResult> DownloadMyCalendar(
        CancellationToken cancellationToken)
    {
        var user = await CurrentUserAsync(cancellationToken);
        if (user is null) return Unauthorized();
        var result = await personalCalendarService.BuildAsync(user, cancellationToken);
        if (result is null) return CalendarProfileNotFound();
        return File(
            result.Content,
            "text/calendar; charset=utf-8",
            $"{SafeFileName(user.DisplayName)}-个人教学日历.ics");
    }

    [HttpGet("calendar/{userId:guid}/{accessToken}.ics")]
    [AllowAnonymous]
    public async Task<ActionResult> GetCalendarFeed(
        Guid userId,
        string accessToken,
        CancellationToken cancellationToken)
    {
        var user = await db.Users.AsNoTracking()
            .FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
        if (user is null ||
            !personalCalendarService.IsAccessTokenValid(user, accessToken))
            return NotFound();

        var result = await personalCalendarService.BuildAsync(user, cancellationToken);
        if (result is null) return NotFound();
        var etag = $"\"{Convert.ToHexString(SHA256.HashData(result.Content))}\"";
        if (Request.Headers.IfNoneMatch.ToString()
            .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Contains(etag, StringComparer.Ordinal))
        {
            return StatusCode(StatusCodes.Status304NotModified);
        }
        Response.Headers.CacheControl = "private, max-age=300";
        Response.Headers.ETag = etag;
        Response.Headers.Append("X-Content-Type-Options", "nosniff");
        return File(result.Content, "text/calendar; charset=utf-8");
    }

    private async Task<ActionResult> BuildTimetableAsync(
        Guid classId,
        Guid? academicTermId,
        Guid? studentId,
        CancellationToken cancellationToken,
        TimetableStudentDto? student = null)
    {
        var result = studentId.HasValue || student is not null
            ? await timetableDataService.BuildAsync(
                TimetableResourceType.Class,
                classId,
                academicTermId,
                null,
                false,
                studentId,
                student,
                cancellationToken)
            : await GetPublishedTimetableAsync(
                TimetableResourceType.Class,
                classId,
                academicTermId,
                cancellationToken);
         return result is null ? NotFound() : Ok(result);
     }

    private Task<TimetableData?> GetPublishedTimetableAsync(
        TimetableResourceType resourceType,
        Guid resourceId,
        Guid? academicTermId,
        CancellationToken cancellationToken) =>
        cache.GetOrCreateAsync(
            AppCacheKeys.PublishedTimetable(
                resourceType.ToString().ToLowerInvariant(),
                resourceId,
                academicTermId),
            token => timetableDataService.BuildAsync(
                resourceType,
                resourceId,
                academicTermId,
                null,
                false,
                null,
                null,
                token),
            AppCacheProfile.PublishedTimetable,
            [AppCacheTags.BaseData, AppCacheTags.Timetables],
            cancellationToken);

    private async Task<TimetableOptionsResponse> LoadOptionsAsync(
        CancellationToken cancellationToken)
    {
        var terms = await db.AcademicTerms.AsNoTracking()
            .Where(x => x.IsEnabled)
            .OrderByDescending(x => x.StartDate)
            .Select(x => new TimetableTermOption(
                x.Id,
                x.Name,
                x.AcademicYear,
                x.Season,
                x.StartDate,
                x.EndDate,
                x.IsCurrent,
                x.IsArchived,
                db.SchedulePlans.Any(plan =>
                    plan.AcademicTermId == x.Id &&
                    plan.Status == SchedulePlanStatus.Published) ||
                db.TeachingTasks.Any(task =>
                    task.AcademicTermId == x.Id &&
                    task.Status == TeachingTaskStatus.Published &&
                    task.SchedulingMode == TeachingTaskSchedulingMode.Flexible) ||
                db.ExamPlans.Any(plan =>
                    plan.AcademicTermId == x.Id &&
                    plan.Status == ExamPlanStatus.Published)))
            .ToListAsync(cancellationToken);
        var defaultTermId = terms.FirstOrDefault(x => x.IsCurrent)?.Id
            ?? terms.FirstOrDefault(x => x.HasPublishedTimetable)?.Id;
        var classes = await db.AdministrativeClasses.AsNoTracking()
            .Where(x => x.IsEnabled && x.Major!.IsEnabled && x.Major.College!.IsEnabled)
            .OrderByDescending(x => x.Grade)
            .ThenBy(x => x.Code)
            .Select(x => new TimetableClassOption(
                x.Id,
                x.Code,
                x.Name,
                x.Grade,
                x.MajorId,
                x.Major!.Name,
                x.Major.CollegeId,
                x.Major.College!.Name,
                defaultTermId.HasValue &&
                (db.ScheduleEntries.Any(entry =>
                     entry.SchedulePlan!.AcademicTermId == defaultTermId.Value &&
                     entry.SchedulePlan.Status == SchedulePlanStatus.Published &&
                     entry.TeachingTask!.Classes.Any(item =>
                         item.AdministrativeClassId == x.Id)) ||
                 db.TeachingTasks.Any(task =>
                     task.AcademicTermId == defaultTermId.Value &&
                     task.Status == TeachingTaskStatus.Published &&
                     task.SchedulingMode == TeachingTaskSchedulingMode.Flexible &&
                     task.Classes.Any(item => item.AdministrativeClassId == x.Id)) ||
                 db.ExamSessions.Any(session =>
                     session.ExamPlan!.AcademicTermId == defaultTermId.Value &&
                     session.ExamPlan.Status == ExamPlanStatus.Published &&
                     session.TeachingTask!.Classes.Any(item =>
                         item.AdministrativeClassId == x.Id)))))
            .ToListAsync(cancellationToken);
        var colleges = await db.Colleges.AsNoTracking()
            .Where(x => x.IsEnabled)
            .OrderBy(x => x.Code)
            .Select(x => new TimetableCollegeOption(x.Id, x.Code, x.Name))
            .ToListAsync(cancellationToken);
        var majors = await db.Majors.AsNoTracking()
            .Where(x => x.IsEnabled && x.College!.IsEnabled)
            .OrderBy(x => x.Code)
            .Select(x => new TimetableMajorOption(
                x.Id, x.Code, x.Name, x.CollegeId))
            .ToListAsync(cancellationToken);
        return new TimetableOptionsResponse(terms, colleges, majors, classes);
    }

    private ActionResult ExcelFile(TimetableData result)
    {
        var bytes = TimetableExcelExporter.Create(result);
        var fileName =
            $"{SafeFileName(result.Subject.Name)}-{SafeFileName(result.Term.Name)}-课表.xlsx";
        return File(bytes, ExcelWorkbookHelper.ContentType, fileName);
    }

    private ActionResult PdfFile(TimetableData result)
    {
        var fileName =
            $"{SafeFileName(result.Subject.Name)}-{SafeFileName(result.Term.Name)}-课表.pdf";
        return File(TimetablePdfExporter.Create(result, officialDocumentOptions.FontPath), "application/pdf", fileName);
    }

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

    private async Task<ApplicationUser?> CurrentUserAsync(
        CancellationToken cancellationToken)
    {
        if (!Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var userId))
            return null;
        return await db.Users.FirstOrDefaultAsync(
            x => x.Id == userId && x.IsEnabled,
            cancellationToken);
    }

    private PersonalCalendarSubscriptionResponse BuildCalendarSubscriptionResponse(
        ApplicationUser user)
    {
        if (string.IsNullOrWhiteSpace(user.CalendarSubscriptionStamp))
            return new(false, null, null, null, null);

        var accessToken = personalCalendarService.CreateAccessToken(user);
        var feedPath =
            $"timetables/calendar/{user.Id:N}/{accessToken}.ics";
        var feedUrl =
            $"{Request.Scheme}://{Request.Host}{Request.PathBase}" +
            $"/api/{feedPath}";
        var webcalUrl = feedUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase)
            ? $"webcal://{feedUrl[8..]}"
            : feedUrl.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
                ? $"webcal://{feedUrl[7..]}"
                : feedUrl;
        return new(
            true,
            feedUrl,
            feedPath,
            webcalUrl,
            user.CalendarSubscriptionCreatedAt);
    }

    private ActionResult CalendarProfileNotFound() => Conflict(new ProblemDetails
    {
        Title = "档案未关联",
        Detail = "当前登录账号没有关联有效的教师或学生档案，请联系教务管理员。",
        Status = StatusCodes.Status409Conflict
    });
}

public sealed record PersonalCalendarSubscriptionResponse(
    bool IsEnabled,
    string? FeedUrl,
    string? FeedPath,
    string? WebcalUrl,
    DateTime? CreatedAt);

public sealed record TimetableOptionsResponse(
    IReadOnlyList<TimetableTermOption> Terms,
    IReadOnlyList<TimetableCollegeOption> Colleges,
    IReadOnlyList<TimetableMajorOption> Majors,
    IReadOnlyList<TimetableClassOption> Classes);

public sealed record TimetableTermOption(
    Guid Id,
    string Name,
    string AcademicYear,
    TermSeason Season,
    DateOnly StartDate,
    DateOnly EndDate,
    bool IsCurrent,
    bool IsArchived,
    bool HasPublishedTimetable);

public sealed record TimetableCollegeOption(Guid Id, string Code, string Name);

public sealed record TimetableMajorOption(
    Guid Id,
    string Code,
    string Name,
    Guid CollegeId);

public sealed record TimetableClassOption(
    Guid Id,
    string Code,
    string Name,
    int Grade,
    Guid MajorId,
    string MajorName,
    Guid CollegeId,
    string CollegeName,
    bool HasPublishedTimetable);
