using ClosedXML.Excel;

namespace Jiaowu.Api.Infrastructure.Timetables;

public static class TimetableExcelExporter
{
    private static readonly string[] Weekdays =
        ["", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"];

    public static byte[] Create(TimetableData timetable)
    {
        using var workbook = new XLWorkbook();
        CreateWorksheet(workbook, timetable, "课表");
        using var stream = new MemoryStream();
        workbook.SaveAs(stream);
        return stream.ToArray();
    }

    public static byte[] Create(IReadOnlyCollection<TimetableData> timetables)
    {
        if (timetables.Count == 0)
            throw new ArgumentException("至少需要一张课表。", nameof(timetables));

        using var workbook = new XLWorkbook();
        var index = 1;
        foreach (var timetable in timetables)
            CreateWorksheet(workbook, timetable, WorksheetName(timetable.Subject.Name, index++));

        using var stream = new MemoryStream();
        workbook.SaveAs(stream);
        return stream.ToArray();
    }

    private static void CreateWorksheet(
        XLWorkbook workbook,
        TimetableData timetable,
        string worksheetName)
    {
        var sheet = workbook.Worksheets.Add(worksheetName);
        sheet.Style.Font.FontName = "Microsoft YaHei";

        sheet.Range("A1:H1").Merge();
        sheet.Cell("A1").Value = $"{timetable.Subject.Name} · {timetable.Term.Name}课表";
        sheet.Cell("A1").Style
            .Font.SetBold()
            .Font.SetFontSize(18)
            .Font.SetFontColor(XLColor.White)
            .Fill.SetBackgroundColor(XLColor.FromHtml("#173E72"))
            .Alignment.SetHorizontal(XLAlignmentHorizontalValues.Left)
            .Alignment.SetVertical(XLAlignmentVerticalValues.Center);
        sheet.Row(1).Height = 34;

        sheet.Range("A2:H2").Merge();
        var activitySummary = string.Concat(
            timetable.ExamEntries.Count > 0
                ? $" · 已融入 {timetable.ExamEntries.Count} 场考试"
                : "",
            timetable.ExperimentEntries.Count > 0
                ? $" · 已融入 {timetable.ExperimentEntries.Count} 场实验"
                : "");
        var hasOneOffActivities =
            timetable.ExamEntries.Count > 0 ||
            timetable.ExperimentEntries.Count > 0;
        sheet.Cell("A2").Value = timetable.Plan is null
            ? hasOneOffActivities
                ? $"固定课表尚未发布{activitySummary} · 导出时间：{DateTime.Now:yyyy-MM-dd HH:mm}"
                : "本学期暂无可用课表版本"
            : $"版本：{timetable.Plan.Version} · 状态：{PlanStatus(timetable.Plan.Status)}{activitySummary} · 导出时间：{DateTime.Now:yyyy-MM-dd HH:mm}";
        sheet.Cell("A2").Style
            .Font.SetFontColor(XLColor.FromHtml("#52677A"))
            .Fill.SetBackgroundColor(XLColor.FromHtml("#EFF3F6"));
        sheet.Row(2).Height = 24;

        var headerRow = 4;
        var headers = new[] { "节次 / 时间" }.Concat(Weekdays.Skip(1)).ToArray();
        for (var column = 1; column <= headers.Length; column++)
        {
            var cell = sheet.Cell(headerRow, column);
            cell.Value = headers[column - 1];
            cell.Style
                .Font.SetBold()
                .Font.SetFontColor(XLColor.White)
                .Fill.SetBackgroundColor(XLColor.FromHtml("#24706A"))
                .Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center)
                .Alignment.SetVertical(XLAlignmentVerticalValues.Center);
        }
        sheet.Row(headerRow).Height = 26;

        var timedEntries = timetable.Entries
            .Concat(timetable.ExamEntries)
            .Concat(timetable.ExperimentEntries)
            .ToArray();
        var periods = timetable.Slots.Select(x => x.PeriodNumber)
            .Concat(timedEntries.SelectMany(x =>
                Enumerable.Range(x.StartPeriod, x.PeriodCount)))
            .Distinct()
            .Order()
            .ToArray();
        if (periods.Length == 0) periods = Enumerable.Range(1, 8).ToArray();
        for (var index = 0; index < periods.Length; index++)
        {
            var period = periods[index];
            var row = headerRow + index + 1;
            var slot = timetable.Slots.FirstOrDefault(x => x.PeriodNumber == period);
            sheet.Cell(row, 1).Value = slot is null
                ? $"第 {period} 节"
                : $"第 {period} 节\n{slot.StartsAt:HH\\:mm}-{slot.EndsAt:HH\\:mm}";
            sheet.Cell(row, 1).Style
                .Font.SetBold()
                .Fill.SetBackgroundColor(XLColor.FromHtml("#F0F4F6"))
                .Alignment.SetHorizontal(XLAlignmentHorizontalValues.Center)
                .Alignment.SetVertical(XLAlignmentVerticalValues.Center)
                .Alignment.SetWrapText();

            for (var day = 1; day <= 7; day++)
            {
                var entries = timedEntries
                    .Where(x =>
                        x.DayOfWeek == day &&
                        x.StartPeriod <= period &&
                        x.StartPeriod + x.PeriodCount - 1 >= period)
                    .ToList();
                var cell = sheet.Cell(row, day + 1);
                cell.Value = string.Join("\n\n", entries.Select(EntryText));
                cell.Style
                    .Alignment.SetVertical(XLAlignmentVerticalValues.Top)
                    .Alignment.SetWrapText();
                if (entries.Count > 0)
                {
                    cell.Style
                        .Fill.SetBackgroundColor(XLColor.FromHtml(
                            entries.Any(x => x.IsExam)
                                ? "#FFF1E8"
                                : entries.Any(x => x.IsExperiment)
                                    ? "#E7F5F1"
                                    : "#E8F3F2"))
                        .Font.SetFontColor(XLColor.FromHtml("#173F4C"));
                }
            }
        }

        var grid = sheet.Range(
            headerRow,
            1,
            headerRow + periods.Length,
            headers.Length);
        grid.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
        grid.Style.Border.InsideBorderColor = XLColor.FromHtml("#D8E1E6");
        grid.Style.Border.OutsideBorder = XLBorderStyleValues.Medium;
        grid.Style.Border.OutsideBorderColor = XLColor.FromHtml("#AABBC5");
        MergeContinuousCells(sheet, timedEntries, periods, headerRow);
        SetRowHeights(sheet, periods.Length, headerRow, headers.Length);
        sheet.Column(1).Width = 16;
        for (var column = 2; column <= 8; column++) sheet.Column(column).Width = 24;
        sheet.SheetView.FreezeRows(headerRow);

        if (timetable.FlexibleCourses.Count > 0)
        {
            var startRow = headerRow + periods.Length + 3;
            sheet.Range(startRow, 1, startRow, 8).Merge();
            sheet.Cell(startRow, 1).Value = "非排时课程（不占正常上课时间与场地）";
            sheet.Cell(startRow, 1).Style
                .Font.SetBold()
                .Font.SetFontColor(XLColor.White)
                .Fill.SetBackgroundColor(XLColor.FromHtml("#805C27"));
            var row = startRow + 1;
            foreach (var course in timetable.FlexibleCourses)
            {
                sheet.Range(row, 1, row, 8).Merge();
                sheet.Cell(row, 1).Value =
                    $"{course.CourseCode} · {course.CourseName} · {string.Join('、', course.TeacherNames)} · 第 {course.StartWeek}-{course.EndWeek} 周 · 每周 {course.WeeklyHours} 学时";
                sheet.Cell(row, 1).Style
                    .Fill.SetBackgroundColor(XLColor.FromHtml("#FFF8EA"))
                    .Alignment.SetWrapText();
                sheet.Row(row).Height = Math.Max(22,
                    8 + sheet.Cell(row, 1).GetString()
                        .Split('\n', StringSplitOptions.None).Length * 15);
                row++;
            }
        }

        sheet.PageSetup.PageOrientation = XLPageOrientation.Landscape;
        sheet.PageSetup.PaperSize = XLPaperSize.A4Paper;
        sheet.PageSetup.FitToPages(1, 0);
        sheet.PageSetup.Margins.SetLeft(0.25).SetRight(0.25).SetTop(0.35).SetBottom(0.35);
    }

    private static string WorksheetName(string subjectName, int index)
    {
        var suffix = $"-{index}";
        var name = string.Concat(subjectName.Select(character =>
            "[]:*?/\\".Contains(character) ? '-' : character)).Trim();
        if (string.IsNullOrWhiteSpace(name)) name = "课表";
        return name.Length > 31 - suffix.Length
            ? name[..(31 - suffix.Length)] + suffix
            : name + suffix;
    }

    private static void MergeContinuousCells(
        IXLWorksheet sheet,
        IReadOnlyCollection<TimetableEntryDto> entries,
        IReadOnlyList<int> periods,
        int headerRow)
    {
        for (var day = 1; day <= 7; day++)
        {
            var start = 0;
            while (start < periods.Count)
            {
                var currentEntries = EntriesAt(entries, day, periods[start]);
                if (currentEntries.Length == 0)
                {
                    start++;
                    continue;
                }

                var end = start;
                while (end + 1 < periods.Count &&
                       periods[end + 1] == periods[end] + 1 &&
                       SameEntries(currentEntries, EntriesAt(entries, day, periods[end + 1])))
                    end++;

                if (end > start)
                    sheet.Range(headerRow + start + 1, day + 1, headerRow + end + 1, day + 1).Merge();
                start = end + 1;
            }
        }
    }

    private static void SetRowHeights(
        IXLWorksheet sheet,
        int periodCount,
        int headerRow,
        int columnCount)
    {
        for (var index = 0; index < periodCount; index++)
        {
            var row = headerRow + index + 1;
            var bodyLineCount = Enumerable.Range(2, columnCount - 1)
                .Select(column => sheet.Cell(row, column).GetString())
                .Where(value => !string.IsNullOrWhiteSpace(value))
                .Select(value => value.Split('\n', StringSplitOptions.None).Length)
                .DefaultIfEmpty(0)
                .Max();
            sheet.Row(row).Height = bodyLineCount == 0
                ? 42
                : Math.Max(52, 14 + bodyLineCount * 15);
        }
    }

    private static TimetableEntryDto[] EntriesAt(
        IReadOnlyCollection<TimetableEntryDto> entries,
        int day,
        int period) =>
        entries.Where(entry =>
                entry.DayOfWeek == day &&
                entry.StartPeriod <= period &&
                entry.StartPeriod + entry.PeriodCount - 1 >= period)
            .OrderBy(entry => entry.Id)
            .ToArray();

    private static bool SameEntries(
        IReadOnlyList<TimetableEntryDto> left,
        IReadOnlyList<TimetableEntryDto> right) =>
        left.Count == right.Count && left.Select(entry => entry.Id).SequenceEqual(right.Select(entry => entry.Id));

    private static string EntryText(TimetableEntryDto entry) =>
        entry.IsExperiment
            ? $"【实验】{entry.ExperimentProjectName}\n" +
              $"{entry.CourseName} · {ArrangementMode(entry)}\n" +
              $"{string.Join('、', entry.TeacherNames)}\n" +
              $"{Location(entry)}\n" +
              $"{entry.ExperimentDate:yyyy-MM-dd} · 第 {entry.StartPeriod}-" +
              $"{entry.StartPeriod + entry.PeriodCount - 1} 节"
            : entry.IsExam
            ? $"【考试】{entry.CourseName}\n" +
              $"{entry.ExamPlanName}\n" +
              $"{string.Join('、', entry.TeacherNames)}\n" +
              $"{Location(entry)}\n" +
              $"{entry.ExamDate:yyyy-MM-dd} · 第 {entry.StartPeriod}-" +
              $"{entry.StartPeriod + entry.PeriodCount - 1} 节"
            : $"{(entry.Kind == Domain.Academic.ScheduleEntryKind.Experiment ? "【实验课】" : "")}{entry.CourseName}\n" +
              $"{string.Join('、', entry.TeacherNames)}\n" +
              $"{Location(entry)}\n" +
              $"{entry.StartWeek}-{entry.EndWeek} 周";

    private static string ArrangementMode(TimetableEntryDto entry) =>
        entry.ExperimentArrangementMode switch
        {
            Domain.Academic.ExperimentArrangementMode.Centralized => "集中安排",
            Domain.Academic.ExperimentArrangementMode.SelfScheduled => "自主预约",
            _ => "实验安排"
        };

    private static string Location(TimetableEntryDto entry) =>
        string.Join(" · ", new[]
        {
            entry.CampusName,
            entry.BuildingName,
            entry.ClassroomName
        }.Where(x => !string.IsNullOrWhiteSpace(x)));

    private static string PlanStatus(Domain.Academic.SchedulePlanStatus status) =>
        status switch
        {
            Domain.Academic.SchedulePlanStatus.Draft => "草稿",
            Domain.Academic.SchedulePlanStatus.Published => "已发布",
            Domain.Academic.SchedulePlanStatus.Archived => "已归档",
            _ => status.ToString()
        };
}
