using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using Jiaowu.Api.Domain.Academic;
using Jiaowu.Api.Domain.Identity;
using Jiaowu.Api.Infrastructure.Auth;
using Jiaowu.Api.Infrastructure.Persistence;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace Jiaowu.Api.Infrastructure.Timetables;

public sealed class PersonalCalendarService(
    AppDbContext db,
    TimetableDataService timetableDataService,
    IOptions<JwtOptions> jwtOptions)
{
    private static readonly TimeSpan ChinaOffset = TimeSpan.FromHours(8);
    private readonly byte[] _signingKey = SHA256.HashData(
        Encoding.UTF8.GetBytes($"{jwtOptions.Value.Key}|personal-calendar"));

    public static string CreateStamp() =>
        Convert.ToHexString(RandomNumberGenerator.GetBytes(32));

    public string CreateAccessToken(ApplicationUser user)
    {
        if (string.IsNullOrWhiteSpace(user.CalendarSubscriptionStamp))
            throw new InvalidOperationException("个人日历订阅尚未启用。");

        var payload = Encoding.UTF8.GetBytes(
            $"{user.Id:N}:{user.CalendarSubscriptionStamp}");
        using var hmac = new HMACSHA256(_signingKey);
        return WebEncoders.Base64UrlEncode(hmac.ComputeHash(payload));
    }

    public bool IsAccessTokenValid(ApplicationUser user, string accessToken)
    {
        if (!user.IsEnabled || string.IsNullOrWhiteSpace(user.CalendarSubscriptionStamp))
            return false;

        byte[] provided;
        try
        {
            provided = WebEncoders.Base64UrlDecode(accessToken);
        }
        catch (FormatException)
        {
            return false;
        }

        var expected = WebEncoders.Base64UrlDecode(CreateAccessToken(user));
        return provided.Length == expected.Length &&
               CryptographicOperations.FixedTimeEquals(provided, expected);
    }

    public async Task<PersonalCalendarResult?> BuildAsync(
        ApplicationUser user,
        CancellationToken cancellationToken)
    {
        var student = await db.Students.AsNoTracking()
            .Where(x => x.UserId == user.Id && x.Status == StudentStatus.Active)
            .Select(x => new
            {
                x.Id,
                x.AdministrativeClassId,
                x.StudentNumber,
                x.Name
            })
            .FirstOrDefaultAsync(cancellationToken);
        var teacher = student is null
            ? await db.Teachers.AsNoTracking()
                .Where(x => x.UserId == user.Id && x.Status == TeacherStatus.Active)
                .Select(x => new { x.Id, x.Name })
                .FirstOrDefaultAsync(cancellationToken)
            : null;
        if (student is null && teacher is null) return null;

        var today = DateOnly.FromDateTime(DateTime.UtcNow.Add(ChinaOffset));
        var earliestDate = today.AddMonths(-2);
        var latestDate = today.AddYears(1);
        var termIds = await db.AcademicTerms.AsNoTracking()
            .Where(x =>
                x.IsEnabled &&
                x.EndDate >= earliestDate &&
                x.StartDate <= latestDate)
            .OrderBy(x => x.StartDate)
            .Select(x => x.Id)
            .ToListAsync(cancellationToken);

        var calendarName = $"明序教务 · {user.DisplayName}个人教学日历";
        var builder = new IcsBuilder(calendarName);
        foreach (var termId in termIds)
        {
            var timetable = student is not null
                ? await timetableDataService.BuildAsync(
                    TimetableResourceType.Class,
                    student.AdministrativeClassId,
                    termId,
                    null,
                    false,
                    student.Id,
                    new TimetableStudentDto(student.StudentNumber, student.Name),
                    cancellationToken)
                : await timetableDataService.BuildAsync(
                    TimetableResourceType.Teacher,
                    teacher!.Id,
                    termId,
                    null,
                    false,
                    null,
                    null,
                    cancellationToken);
            if (timetable is null) continue;

            AddScheduledCourses(builder, timetable);
            AddExamEntries(builder, timetable, teacher is not null);
            AddExperimentEntries(builder, timetable);
            AddFlexibleCourseReminders(builder, timetable);
        }

        var makeupSessions = await LoadMakeupSessionsAsync(
            student?.Id,
            teacher?.Id,
            earliestDate,
            latestDate,
            cancellationToken);
        foreach (var session in makeupSessions)
        {
            builder.AddTimedEvent(
                $"makeup-{session.Id:N}@jiaowu",
                teacher is null
                    ? $"[补考] {session.CourseName}"
                    : $"[补考监考] {session.CourseName}",
                ToUtc(session.ExamDate, TimeOnly.FromDateTime(session.StartsAt)),
                ToUtc(session.ExamDate, TimeOnly.FromDateTime(session.EndsAt)),
                JoinLocation(
                    session.CampusName,
                    session.BuildingName,
                    session.ClassroomName),
                JoinDescription(
                    session.PlanName,
                    session.TaskName,
                    session.Notes),
                "补考",
                session.UpdatedAt);
        }

        return new PersonalCalendarResult(
            builder.Build(),
            calendarName,
            builder.EventCount);
    }

    private static void AddScheduledCourses(IcsBuilder builder, TimetableData timetable)
    {
        var slots = timetable.Slots.ToDictionary(x => x.PeriodNumber);
        var termMonday = StartOfWeek(timetable.Term.StartDate);
        foreach (var entry in timetable.Entries)
        {
            if (!slots.TryGetValue(entry.StartPeriod, out var startSlot) ||
                !slots.TryGetValue(
                    entry.StartPeriod + entry.PeriodCount - 1,
                    out var endSlot))
                continue;

            for (var week = entry.StartWeek; week <= entry.EndWeek; week++)
            {
                if (!OccursInWeek(entry.WeekPattern, week)) continue;
                var date = termMonday.AddDays((week - 1) * 7 + entry.DayOfWeek - 1);
                if (date < timetable.Term.StartDate || date > timetable.Term.EndDate)
                    continue;

                builder.AddTimedEvent(
                    $"course-{entry.Id:N}-{date:yyyyMMdd}@jiaowu",
                    entry.CourseName,
                    ToUtc(date, startSlot.StartsAt),
                    ToUtc(date, endSlot.EndsAt),
                    JoinLocation(
                        entry.CampusName,
                        entry.BuildingName,
                        entry.ClassroomName),
                    JoinDescription(
                        entry.TaskName,
                        $"教师：{string.Join('、', entry.TeacherNames)}",
                        $"教学班：{string.Join('、', entry.ClassNames)}",
                        entry.Notes),
                    "课程",
                    entry.UpdatedAt);
            }
        }
    }

    private static void AddExamEntries(
        IcsBuilder builder,
        TimetableData timetable,
        bool isTeacher)
    {
        var slots = timetable.Slots.ToDictionary(x => x.PeriodNumber);
        foreach (var entry in timetable.ExamEntries)
        {
            if (entry.ExamDate is not DateOnly examDate ||
                !slots.TryGetValue(entry.StartPeriod, out var startSlot) ||
                !slots.TryGetValue(
                    entry.StartPeriod + entry.PeriodCount - 1,
                    out var endSlot))
                continue;

            builder.AddTimedEvent(
                $"exam-{entry.Id:N}@jiaowu",
                isTeacher
                    ? $"[监考] {entry.CourseName}"
                    : $"[考试] {entry.CourseName}",
                ToUtc(examDate, startSlot.StartsAt),
                ToUtc(examDate, endSlot.EndsAt),
                JoinLocation(
                    entry.CampusName,
                    entry.BuildingName,
                    entry.ClassroomName),
                JoinDescription(
                    entry.ExamPlanName,
                    entry.TaskName,
                    string.Join('、', entry.TeacherNames),
                    entry.Notes),
                "考试",
                entry.UpdatedAt);
        }
    }

    private static void AddFlexibleCourseReminders(
        IcsBuilder builder,
        TimetableData timetable)
    {
        foreach (var course in timetable.FlexibleCourses)
        {
            builder.AddAllDayEvent(
                $"flexible-{course.Id:N}-{timetable.Term.Id:N}@jiaowu",
                $"[灵活安排] {course.CourseName}",
                timetable.Term.StartDate,
                JoinDescription(
                    $"{course.StartWeek}—{course.EndWeek} 周",
                    $"每周 {course.WeeklyHours} 学时",
                    $"教师：{string.Join('、', course.TeacherNames)}",
                    course.Notes),
                "灵活课程",
                course.UpdatedAt);
        }
    }

    private static void AddExperimentEntries(
        IcsBuilder builder,
        TimetableData timetable)
    {
        var slots = timetable.Slots.ToDictionary(x => x.PeriodNumber);
        foreach (var entry in timetable.ExperimentEntries)
        {
            if (entry.ExperimentDate is not DateOnly experimentDate ||
                !slots.TryGetValue(entry.StartPeriod, out var startSlot) ||
                !slots.TryGetValue(
                    entry.StartPeriod + entry.PeriodCount - 1,
                    out var endSlot))
                continue;

            var arrangement = entry.ExperimentArrangementMode switch
            {
                ExperimentArrangementMode.Centralized => "集中安排",
                ExperimentArrangementMode.SelfScheduled => "自主预约",
                _ => "实验安排"
            };
            builder.AddTimedEvent(
                $"experiment-{entry.Id:N}@jiaowu",
                $"[实验] {entry.ExperimentProjectName ?? entry.CourseName}",
                ToUtc(experimentDate, startSlot.StartsAt),
                ToUtc(experimentDate, endSlot.EndsAt),
                JoinLocation(
                    entry.CampusName,
                    entry.BuildingName,
                    entry.ClassroomName),
                JoinDescription(
                    $"{entry.ExperimentProjectCode} · {arrangement}",
                    entry.CourseName,
                    entry.TaskName,
                    $"教师：{string.Join('、', entry.TeacherNames)}",
                    entry.Notes),
                "实验",
                entry.UpdatedAt);
        }
    }

    private async Task<List<PersonalMakeupSession>> LoadMakeupSessionsAsync(
        Guid? studentId,
        Guid? teacherId,
        DateOnly earliestDate,
        DateOnly latestDate,
        CancellationToken cancellationToken)
    {
        if (!studentId.HasValue && !teacherId.HasValue) return [];
        var source = db.MakeupExamSessions.AsNoTracking()
            .Where(x =>
                x.MakeupExamPlan!.Status == MakeupExamPlanStatus.Published &&
                x.ExamDate >= earliestDate &&
                x.ExamDate <= latestDate);
        source = studentId.HasValue
            ? source.Where(x => x.Enrollments.Any(e => e.StudentId == studentId.Value))
            : source.Where(x => x.Invigilators.Any(i => i.TeacherId == teacherId!.Value));

        return await source
            .OrderBy(x => x.ExamDate)
            .ThenBy(x => x.StartsAt)
            .Select(x => new PersonalMakeupSession(
                x.Id,
                x.MakeupExamPlan!.Name,
                x.TeachingTask!.Name,
                x.TeachingTask.Course!.Name,
                x.ExamDate,
                x.StartsAt,
                x.EndsAt,
                x.Classroom == null ? null : x.Classroom.Name,
                x.Classroom == null ? null : x.Classroom.Building!.Name,
                x.Classroom == null ? null : x.Classroom.Building!.Campus!.Name,
                x.Notes,
                x.UpdatedAt))
            .ToListAsync(cancellationToken);
    }

    private static DateOnly StartOfWeek(DateOnly date)
    {
        var day = date.DayOfWeek == DayOfWeek.Sunday ? 7 : (int)date.DayOfWeek;
        return date.AddDays(1 - day);
    }

    private static bool OccursInWeek(WeekPattern pattern, int week) => pattern switch
    {
        WeekPattern.Odd => week % 2 == 1,
        WeekPattern.Even => week % 2 == 0,
        _ => true
    };

    private static DateTime ToUtc(DateOnly date, TimeOnly time)
    {
        var local = date.ToDateTime(time, DateTimeKind.Unspecified);
        return new DateTimeOffset(local, ChinaOffset).UtcDateTime;
    }

    private static string JoinLocation(params string?[] parts) =>
        string.Join(" · ", parts.Where(x => !string.IsNullOrWhiteSpace(x)));

    private static string JoinDescription(params string?[] parts) =>
        string.Join("\n", parts.Where(x => !string.IsNullOrWhiteSpace(x)));

    private sealed record PersonalMakeupSession(
        Guid Id,
        string PlanName,
        string TaskName,
        string CourseName,
        DateOnly ExamDate,
        DateTime StartsAt,
        DateTime EndsAt,
        string? ClassroomName,
        string? BuildingName,
        string? CampusName,
        string? Notes,
        DateTime UpdatedAt);
}

public sealed record PersonalCalendarResult(
    byte[] Content,
    string Name,
    int EventCount);

internal sealed class IcsBuilder
{
    private static readonly UTF8Encoding Utf8 = new(false);
    private readonly StringBuilder _content = new();
    private bool _built;

    public IcsBuilder(string name)
    {
        AddLine("BEGIN:VCALENDAR");
        AddLine("VERSION:2.0");
        AddLine("PRODID:-//Mingxu Jiaowu//Personal Teaching Calendar//ZH-CN");
        AddLine("CALSCALE:GREGORIAN");
        AddLine("METHOD:PUBLISH");
        AddLine($"X-WR-CALNAME:{Escape(name)}");
        AddLine("X-WR-TIMEZONE:Asia/Shanghai");
        AddLine("REFRESH-INTERVAL;VALUE=DURATION:PT1H");
        AddLine("X-PUBLISHED-TTL:PT1H");
    }

    public int EventCount { get; private set; }

    public void AddTimedEvent(
        string uid,
        string summary,
        DateTime startsAtUtc,
        DateTime endsAtUtc,
        string location,
        string description,
        string category,
        DateTime updatedAt)
    {
        AddLine("BEGIN:VEVENT");
        AddLine($"UID:{uid}");
        AddLine($"DTSTAMP:{FormatUtc(NormalizeUpdatedAt(updatedAt))}");
        AddLine($"LAST-MODIFIED:{FormatUtc(NormalizeUpdatedAt(updatedAt))}");
        AddLine($"DTSTART:{FormatUtc(startsAtUtc)}");
        AddLine($"DTEND:{FormatUtc(endsAtUtc)}");
        AddLine($"SUMMARY:{Escape(summary)}");
        if (!string.IsNullOrWhiteSpace(location))
            AddLine($"LOCATION:{Escape(location)}");
        if (!string.IsNullOrWhiteSpace(description))
            AddLine($"DESCRIPTION:{Escape(description)}");
        AddLine($"CATEGORIES:{Escape(category)}");
        AddLine("STATUS:CONFIRMED");
        AddLine("END:VEVENT");
        EventCount++;
    }

    public void AddAllDayEvent(
        string uid,
        string summary,
        DateOnly date,
        string description,
        string category,
        DateTime updatedAt)
    {
        AddLine("BEGIN:VEVENT");
        AddLine($"UID:{uid}");
        AddLine($"DTSTAMP:{FormatUtc(NormalizeUpdatedAt(updatedAt))}");
        AddLine($"LAST-MODIFIED:{FormatUtc(NormalizeUpdatedAt(updatedAt))}");
        AddLine($"DTSTART;VALUE=DATE:{date:yyyyMMdd}");
        AddLine($"DTEND;VALUE=DATE:{date.AddDays(1):yyyyMMdd}");
        AddLine($"SUMMARY:{Escape(summary)}");
        if (!string.IsNullOrWhiteSpace(description))
            AddLine($"DESCRIPTION:{Escape(description)}");
        AddLine($"CATEGORIES:{Escape(category)}");
        AddLine("STATUS:CONFIRMED");
        AddLine("TRANSP:TRANSPARENT");
        AddLine("END:VEVENT");
        EventCount++;
    }

    public byte[] Build()
    {
        if (!_built)
        {
            AddLine("END:VCALENDAR");
            _built = true;
        }
        return Utf8.GetBytes(_content.ToString());
    }

    private void AddLine(string line)
    {
        var current = new StringBuilder();
        var byteCount = 0;
        foreach (var rune in line.EnumerateRunes())
        {
            var value = rune.ToString();
            var runeBytes = Utf8.GetByteCount(value);
            if (byteCount > 0 && byteCount + runeBytes > 74)
            {
                _content.Append(current).Append("\r\n ");
                current.Clear();
                byteCount = 1;
            }
            current.Append(value);
            byteCount += runeBytes;
        }
        _content.Append(current).Append("\r\n");
    }

    private static string Escape(string value) => value
        .Replace("\\", "\\\\", StringComparison.Ordinal)
        .Replace("\r\n", "\\n", StringComparison.Ordinal)
        .Replace("\n", "\\n", StringComparison.Ordinal)
        .Replace(";", "\\;", StringComparison.Ordinal)
        .Replace(",", "\\,", StringComparison.Ordinal);

    private static string FormatUtc(DateTime value) =>
        value.ToUniversalTime().ToString(
            "yyyyMMdd'T'HHmmss'Z'",
            CultureInfo.InvariantCulture);

    private static DateTime NormalizeUpdatedAt(DateTime value) =>
        value == default
            ? new DateTime(2020, 1, 1, 0, 0, 0, DateTimeKind.Utc)
            : value.Kind == DateTimeKind.Unspecified
                ? DateTime.SpecifyKind(value, DateTimeKind.Utc)
                : value.ToUniversalTime();
}
