package com.magistr.app.service;

import com.magistr.app.dto.RenderedLessonDto;
import com.magistr.app.model.*;
import com.magistr.app.repository.GroupRepository;
import com.magistr.app.repository.ScheduleRuleRepository;
import com.magistr.app.repository.TimeSlotDateAssignmentRepository;
import com.magistr.app.repository.TimeSlotRepository;
import com.magistr.app.repository.TimeSlotScopeRepository;
import com.magistr.app.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.TextStyle;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class ScheduleGeneratorService {

    private static final Logger logger = LoggerFactory.getLogger(ScheduleGeneratorService.class);
    private static final int ACADEMIC_HOURS_PER_SLOT = 2;
    private static final long MAX_RANGE_DAYS = 120;
    private static final String APPLY_MODE_WEEKDAY = "WEEKDAY";

    private final ScheduleRuleRepository scheduleRuleRepository;
    private final GroupRepository groupRepository;
    private final UserRepository userRepository;
    private final AcademicDateService academicDateService;
    private final TimeSlotRepository timeSlotRepository;
    private final TimeSlotScopeRepository timeSlotScopeRepository;
    private final TimeSlotDateAssignmentRepository dateAssignmentRepository;
    private final BusinessTimeService businessTime;

    public ScheduleGeneratorService(ScheduleRuleRepository scheduleRuleRepository,
                                    GroupRepository groupRepository,
                                    UserRepository userRepository,
                                    AcademicDateService academicDateService,
                                    TimeSlotRepository timeSlotRepository,
                                    TimeSlotScopeRepository timeSlotScopeRepository,
                                    TimeSlotDateAssignmentRepository dateAssignmentRepository) {
        this(scheduleRuleRepository, groupRepository, userRepository, academicDateService,
                timeSlotRepository, timeSlotScopeRepository, dateAssignmentRepository,
                BusinessTimeService.systemDefault());
    }

    @Autowired
    public ScheduleGeneratorService(ScheduleRuleRepository scheduleRuleRepository,
                                    GroupRepository groupRepository,
                                    UserRepository userRepository,
                                    AcademicDateService academicDateService,
                                    TimeSlotRepository timeSlotRepository,
                                    TimeSlotScopeRepository timeSlotScopeRepository,
                                    TimeSlotDateAssignmentRepository dateAssignmentRepository,
                                    BusinessTimeService businessTime) {
        this.scheduleRuleRepository = scheduleRuleRepository;
        this.groupRepository = groupRepository;
        this.userRepository = userRepository;
        this.academicDateService = academicDateService;
        this.timeSlotRepository = timeSlotRepository;
        this.timeSlotScopeRepository = timeSlotScopeRepository;
        this.dateAssignmentRepository = dateAssignmentRepository;
        this.businessTime = businessTime;
    }

    public List<RenderedLessonDto> buildScheduleForGroup(Long groupId, LocalDate startDate, LocalDate endDate) {
        validateRange(startDate, endDate);
        StudentGroup group = groupRepository.findById(groupId)
                .orElseThrow(() -> new IllegalArgumentException("Группа не найдена"));
        return buildScheduleForGroupsInternal(List.of(group), startDate, endDate);
    }

    public List<RenderedLessonDto> buildScheduleForGroups(Collection<StudentGroup> groups,
                                                          LocalDate startDate,
                                                          LocalDate endDate) {
        validateRange(startDate, endDate);
        List<StudentGroup> groupSnapshot = Optional.ofNullable(groups)
                .orElseGet(List::of)
                .stream()
                .filter(Objects::nonNull)
                .filter(group -> group.getId() != null)
                .collect(Collectors.toMap(
                        StudentGroup::getId,
                        group -> group,
                        (first, second) -> first,
                        LinkedHashMap::new
                ))
                .values()
                .stream()
                .toList();
        return buildScheduleForGroupsInternal(groupSnapshot, startDate, endDate);
    }

    private List<RenderedLessonDto> buildScheduleForGroupsInternal(List<StudentGroup> groups,
                                                                   LocalDate startDate,
                                                                   LocalDate endDate) {
        if (groups.isEmpty()) {
            return List.of();
        }

        List<Semester> semesters = academicDateService.findSemesters(startDate, endDate);
        if (semesters.isEmpty()) {
            return List.of();
        }
        Set<Long> semesterIds = semesters.stream()
                .map(Semester::getId)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        Set<Long> groupIds = groups.stream()
                .map(StudentGroup::getId)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        List<ScheduleRule> rules = scheduleRuleRepository.findByGroupIdsAndSemesterIds(groupIds, semesterIds);
        AcademicDateService.ScheduleSnapshot dateSnapshot = academicDateService
                .createScheduleSnapshot(groups, semesters, endDate);
        TimeSlotSnapshot timeSlotSnapshot = loadTimeSlotSnapshot(startDate, endDate, rules);
        Map<GroupSemesterKey, List<ScheduleRule>> rulesByGroupAndSemester = indexRulesByGroupAndSemester(
                rules,
                groupIds
        );

        List<RenderedLessonDto> result = new ArrayList<>();
        for (StudentGroup group : groups) {
            result.addAll(buildScheduleForGroup(
                    group,
                    startDate,
                    endDate,
                    dateSnapshot,
                    timeSlotSnapshot,
                    rulesByGroupAndSemester
            ));
        }
        return result;
    }

    private List<RenderedLessonDto> buildScheduleForGroup(StudentGroup group,
                                                          LocalDate startDate,
                                                          LocalDate endDate,
                                                          AcademicDateService.ScheduleSnapshot dateSnapshot,
                                                          TimeSlotSnapshot timeSlotSnapshot,
                                                          Map<GroupSemesterKey, List<ScheduleRule>> rulesByGroupAndSemester) {

        Map<String, Integer> consumedHoursProgress = new HashMap<>();
        List<RenderedLessonDto> result = new ArrayList<>();
        Set<Long> warnedAcademicYears = new HashSet<>();
        Set<Long> primedSemesterIds = new HashSet<>();

        for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
            Optional<Semester> semesterOpt = dateSnapshot.findSemester(date);
            if (semesterOpt.isEmpty()) {
                continue;
            }

            Semester semester = semesterOpt.get();
            if (!dateSnapshot.hasCalendarAssignment(group, semester)
                    && warnedAcademicYears.add(semester.getAcademicYear().getId())) {
                logger.info("Для группы '{}' не назначен календарный учебный график на учебный год {}",
                        group.getName(), semester.getAcademicYear().getTitle());
            }
            if (!dateSnapshot.isTheoryDay(group, semester, date)) {
                continue;
            }

            List<ScheduleRule> rules = rulesByGroupAndSemester.getOrDefault(
                    new GroupSemesterKey(group.getId(), semester.getId()),
                    List.of()
            );
            if (primedSemesterIds.add(semester.getId())) {
                primeConsumedHoursBeforeRange(
                        semester,
                        rules,
                        startDate,
                        group,
                        null,
                        consumedHoursProgress,
                        dateSnapshot,
                        timeSlotSnapshot
                );
            }

            for (ScheduleRule rule : sortRules(rules)) {
                processRuleForDate(
                        rule,
                        semester,
                        date,
                        group,
                        null,
                        consumedHoursProgress,
                        result,
                        dateSnapshot,
                        timeSlotSnapshot
                );
            }
        }

        return sortRenderedLessons(result);
    }

    public List<RenderedLessonDto> buildScheduleForTeacher(Long teacherId, LocalDate startDate, LocalDate endDate) {
        validateRange(startDate, endDate);
        if (!userRepository.existsById(teacherId)) {
            throw new IllegalArgumentException("Преподаватель не найден");
        }

        List<Semester> semesters = academicDateService.findSemesters(startDate, endDate);
        if (semesters.isEmpty()) {
            return List.of();
        }
        Set<Long> semesterIds = semesters.stream()
                .map(Semester::getId)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        List<ScheduleRule> rules = scheduleRuleRepository.findByTeacherIdAndSemesterIds(teacherId, semesterIds);
        List<StudentGroup> groups = rules.stream()
                .flatMap(rule -> rule.getGroups().stream())
                .filter(group -> group.getId() != null)
                .collect(Collectors.toMap(
                        StudentGroup::getId,
                        group -> group,
                        (first, second) -> first,
                        LinkedHashMap::new
                ))
                .values()
                .stream()
                .toList();
        AcademicDateService.ScheduleSnapshot dateSnapshot = academicDateService
                .createScheduleSnapshot(groups, semesters, endDate);
        TimeSlotSnapshot timeSlotSnapshot = loadTimeSlotSnapshot(startDate, endDate, rules);
        Map<Long, List<ScheduleRule>> rulesBySemester = rules.stream()
                .collect(Collectors.groupingBy(rule -> rule.getSemester().getId()));
        Map<String, Integer> consumedHoursProgress = new HashMap<>();
        List<RenderedLessonDto> result = new ArrayList<>();
        Set<Long> primedSemesterIds = new HashSet<>();

        for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
            Optional<Semester> semesterOpt = dateSnapshot.findSemester(date);
            if (semesterOpt.isEmpty()) {
                continue;
            }

            Semester semester = semesterOpt.get();
            List<ScheduleRule> semesterRules = rulesBySemester.getOrDefault(semester.getId(), List.of());
            if (primedSemesterIds.add(semester.getId())) {
                primeConsumedHoursBeforeRange(
                        semester,
                        semesterRules,
                        startDate,
                        null,
                        teacherId,
                        consumedHoursProgress,
                        dateSnapshot,
                        timeSlotSnapshot
                );
            }

            for (ScheduleRule rule : sortRules(semesterRules)) {
                processRuleForDate(
                        rule,
                        semester,
                        date,
                        null,
                        teacherId,
                        consumedHoursProgress,
                        result,
                        dateSnapshot,
                        timeSlotSnapshot
                );
            }
        }

        return sortRenderedLessons(result);
    }

    public void clearCache() {
        // Расход часов считается в контексте одного построения расписания.
        // Метод оставлен для совместимости с контроллерами, которые сигнализируют об изменении исходных данных.
    }

    private void validateRange(LocalDate startDate, LocalDate endDate) {
        if (startDate == null || endDate == null) {
            throw new IllegalArgumentException("Дата начала и дата окончания обязательны");
        }
        if (endDate.isBefore(startDate)) {
            throw new IllegalArgumentException("Дата окончания не может быть раньше даты начала");
        }
        long inclusiveDays = java.time.temporal.ChronoUnit.DAYS.between(startDate, endDate) + 1;
        if (inclusiveDays > MAX_RANGE_DAYS) {
            throw new IllegalArgumentException("Диапазон расписания не может превышать 120 дней");
        }
    }

    private List<ScheduleRule> sortRules(List<ScheduleRule> rules) {
        return rules.stream()
                .sorted(Comparator
                        .comparing((ScheduleRule rule) -> rule.getSubject().getName(), Comparator.nullsLast(String::compareTo))
                        .thenComparing(ScheduleRule::getId))
                .toList();
    }

    private List<RenderedLessonDto> sortRenderedLessons(List<RenderedLessonDto> lessons) {
        return lessons.stream()
                .sorted(Comparator
                        .comparing(RenderedLessonDto::date)
                        .thenComparing(RenderedLessonDto::timeSlotOrder, Comparator.nullsLast(Integer::compareTo))
                        .thenComparing(RenderedLessonDto::subjectName, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private Map<GroupSemesterKey, List<ScheduleRule>> indexRulesByGroupAndSemester(
            Collection<ScheduleRule> rules,
            Set<Long> requestedGroupIds
    ) {
        Map<GroupSemesterKey, List<ScheduleRule>> result = new HashMap<>();
        for (ScheduleRule rule : rules) {
            Long semesterId = rule.getSemester().getId();
            for (StudentGroup group : rule.getGroups()) {
                if (!requestedGroupIds.contains(group.getId())) {
                    continue;
                }
                result.computeIfAbsent(
                        new GroupSemesterKey(group.getId(), semesterId),
                        ignored -> new ArrayList<>()
                ).add(rule);
            }
        }
        result.replaceAll((key, value) -> sortRules(value));
        return result;
    }

    private TimeSlotSnapshot loadTimeSlotSnapshot(LocalDate startDate,
                                                  LocalDate endDate,
                                                  Collection<ScheduleRule> rules) {
        if (rules.isEmpty()) {
            return TimeSlotSnapshot.empty();
        }
        return new TimeSlotSnapshot(
                dateAssignmentRepository.findForScheduleRange(startDate, endDate),
                timeSlotScopeRepository.findByApplyModeOrderByDisplayOrderAsc(APPLY_MODE_WEEKDAY),
                timeSlotRepository.findAllForSchedule()
        );
    }

    private void primeConsumedHoursBeforeRange(Semester semester,
                                               List<ScheduleRule> rules,
                                               LocalDate rangeStart,
                                               StudentGroup targetGroup,
                                               Long targetTeacherId,
                                               Map<String, Integer> consumedHoursProgress,
                                               AcademicDateService.ScheduleSnapshot dateSnapshot,
                                               TimeSlotSnapshot timeSlotSnapshot) {
        LocalDate endExclusive = minDate(rangeStart, semester.getEndDate().plusDays(1));
        if (!semester.getStartDate().isBefore(endExclusive)) {
            return;
        }

        List<ScheduleRule> sortedRules = sortRules(rules);
        for (LocalDate date = semester.getStartDate(); date.isBefore(endExclusive); date = date.plusDays(1)) {
            for (ScheduleRule rule : sortedRules) {
                processRuleForDate(
                        rule,
                        semester,
                        date,
                        targetGroup,
                        targetTeacherId,
                        consumedHoursProgress,
                        null,
                        dateSnapshot,
                        timeSlotSnapshot
                );
            }
        }
    }

    private LocalDate minDate(LocalDate first, LocalDate second) {
        return first.isBefore(second) ? first : second;
    }

    private void processRuleForDate(ScheduleRule rule,
                                    Semester semester,
                                    LocalDate date,
                                    StudentGroup targetGroup,
                                    Long targetTeacherId,
                                    Map<String, Integer> consumedHoursProgress,
                                    List<RenderedLessonDto> result,
                                    AcademicDateService.ScheduleSnapshot dateSnapshot,
                                    TimeSlotSnapshot timeSlotSnapshot) {
        if (rule.getLectureAcademicHours() == null
                || rule.getLaboratoryAcademicHours() == null
                || rule.getPracticeAcademicHours() == null) {
            return;
        }

        if (semester == null || !Objects.equals(semester.getId(), rule.getSemester().getId())) {
            return;
        }
        if (!rule.getSubject().isActiveOn(date)) {
            return;
        }
        if (!rule.isActiveOn(date)) {
            return;
        }

        List<StudentGroup> eligibleGroups = eligibleGroups(rule, targetGroup, semester, date, dateSnapshot);
        if (eligibleGroups.isEmpty()) {
            return;
        }

        ScheduleParity dateParity = academicDateService.getParity(semester, date);
        int weekNumber = academicDateService.getWeekNumber(semester, date);
        int dayOfWeek = date.getDayOfWeek().getValue();
        List<ScheduleRuleSlot> slots = rule.getSlots().stream()
                .filter(slot -> Objects.equals(slot.getDayOfWeek(), dayOfWeek))
                .filter(slot -> slot.getParity() == ScheduleParity.BOTH || slot.getParity() == dateParity)
                .filter(slot -> targetTeacherId == null || Objects.equals(slot.getTeacher().getId(), targetTeacherId))
                .sorted(Comparator
                        .comparing((ScheduleRuleSlot slot) -> slot.getTimeSlot().getOrderNumber(), Comparator.nullsLast(Integer::compareTo))
                        .thenComparing(ScheduleRuleSlot::getId))
                .toList();

        if (slots.isEmpty()) {
            return;
        }

        for (ScheduleRuleSlot slot : slots) {
            if (!slot.getTeacher().isActiveOn(date) || !slot.getClassroom().isActiveOn(date)) {
                continue;
            }
            if (date.isAfter(businessTime.today()) && Boolean.FALSE.equals(slot.getClassroom().getIsAvailable())) {
                continue;
            }
            ScheduleLessonCategory category = ScheduleLessonCategory.fromLessonType(slot.getLessonType());
            if (weekNumber < rule.startWeekFor(category)) {
                continue;
            }
            int totalHours = rule.academicHoursFor(category);
            if (totalHours <= 0) {
                continue;
            }
            List<LessonScope> lessonScopes = lessonScopesForSlot(slot, category, eligibleGroups);
            if (lessonScopes.isEmpty()) {
                continue;
            }

            List<ActiveLessonScope> activeScopes = new ArrayList<>();
            for (LessonScope scope : lessonScopes) {
                String progressKey = consumptionProgressKey(rule, targetGroup, category, scope.subgroupId());
                int consumedHours = consumedHoursProgress.getOrDefault(progressKey, 0);
                if (consumedHours >= totalHours) {
                    continue;
                }
                int remainingAfterLesson = Math.max(0, totalHours - consumedHours - ACADEMIC_HOURS_PER_SLOT);
                activeScopes.add(new ActiveLessonScope(scope, consumedHours, remainingAfterLesson));
            }

            if (activeScopes.isEmpty()) {
                continue;
            }

            if (result != null) {
                Map<Long, StudentGroup> lessonGroupById = new LinkedHashMap<>();
                Map<Long, Subgroup> lessonSubgroupById = new LinkedHashMap<>();
                activeScopes.forEach(scope -> {
                    scope.scope().groups().forEach(group -> lessonGroupById.putIfAbsent(group.getId(), group));
                    scope.scope().subgroups().forEach(subgroup -> lessonSubgroupById.putIfAbsent(subgroup.getId(), subgroup));
                });
                List<StudentGroup> lessonGroups = new ArrayList<>(lessonGroupById.values());
                List<Subgroup> lessonSubgroups = new ArrayList<>(lessonSubgroupById.values());
                int consumedHours = activeScopes.stream()
                        .mapToInt(ActiveLessonScope::consumedHours)
                        .min()
                        .orElse(0);
                int remainingAfterLesson = activeScopes.stream()
                        .mapToInt(ActiveLessonScope::remainingAfterLesson)
                        .min()
                        .orElse(0);

                result.add(toRenderedLesson(rule, slot, date, weekNumber, dateParity, lessonGroups, lessonSubgroups,
                        totalHours, consumedHours, remainingAfterLesson, timeSlotSnapshot));
            }
            for (ActiveLessonScope scope : activeScopes) {
                consumedHoursProgress.put(
                        consumptionProgressKey(rule, targetGroup, category, scope.scope().subgroupId()),
                        scope.consumedHours() + ACADEMIC_HOURS_PER_SLOT
                );
            }
        }
    }

    private List<StudentGroup> eligibleGroups(ScheduleRule rule,
                                              StudentGroup targetGroup,
                                              Semester semester,
                                              LocalDate date,
                                              AcademicDateService.ScheduleSnapshot dateSnapshot) {
        if (targetGroup != null) {
            return targetGroup.isActiveOn(date) && dateSnapshot.isTheoryDay(targetGroup, semester, date)
                    ? List.of(targetGroup)
                    : List.of();
        }

        return rule.getGroups().stream()
                .filter(group -> group.isActiveOn(date))
                .filter(group -> dateSnapshot.isTheoryDay(group, semester, date))
                .sorted(Comparator.comparing(StudentGroup::getName))
                .toList();
    }

    private String consumptionBucket(ScheduleLessonCategory category, Long subgroupId) {
        return category.name() + ":" + (subgroupId == null ? "whole" : subgroupId);
    }

    private String consumptionProgressKey(ScheduleRule rule,
                                          StudentGroup targetGroup,
                                          ScheduleLessonCategory category,
                                          Long subgroupId) {
        Long groupId = targetGroup == null ? null : targetGroup.getId();
        return rule.getId()
                + ":" + consumptionBucket(category, subgroupId)
                + ":" + (groupId == null ? "all" : groupId);
    }

    private List<LessonScope> lessonScopesForSlot(ScheduleRuleSlot slot,
                                                  ScheduleLessonCategory category,
                                                  List<StudentGroup> eligibleGroups) {
        if (category != ScheduleLessonCategory.LABORATORY || slot.getSubgroups().isEmpty()) {
            return eligibleGroups.isEmpty()
                    ? List.of()
                    : List.of(new LessonScope(eligibleGroups, List.of(), null));
        }

        Set<Long> eligibleGroupIds = eligibleGroups.stream()
                .map(StudentGroup::getId)
                .collect(Collectors.toSet());
        Map<Long, StudentGroup> eligibleGroupById = eligibleGroups.stream()
                .collect(Collectors.toMap(StudentGroup::getId, group -> group));

        return slot.getSubgroups().stream()
                .filter(subgroup -> eligibleGroupIds.contains(subgroup.getStudentGroup().getId()))
                .sorted(Comparator
                        .comparing((Subgroup subgroup) -> subgroup.getStudentGroup().getName())
                        .thenComparing(Subgroup::getName))
                .map(subgroup -> new LessonScope(
                        List.of(eligibleGroupById.get(subgroup.getStudentGroup().getId())),
                        List.of(subgroup),
                        subgroup.getId()
                ))
                .toList();
    }

    private record LessonScope(List<StudentGroup> groups, List<Subgroup> subgroups, Long subgroupId) {
    }

    private record ActiveLessonScope(LessonScope scope, int consumedHours, int remainingAfterLesson) {
    }

    private record GroupSemesterKey(Long groupId, Long semesterId) {
    }

    private record ScopeOrderKey(Long scopeId, Integer orderNumber) {
    }

    private RenderedLessonDto toRenderedLesson(ScheduleRule rule,
                                               ScheduleRuleSlot slot,
                                               LocalDate date,
                                               int weekNumber,
                                               ScheduleParity dateParity,
                                               List<StudentGroup> groups,
                                               List<Subgroup> subgroups,
                                               int lessonTypeAcademicHours,
                                               int consumedBeforeLesson,
                                               int remainingAfterLesson,
                                               TimeSlotSnapshot timeSlotSnapshot) {
        TimeSlot timeSlot = timeSlotSnapshot.resolve(slot.getTimeSlot(), date);
        List<StudentGroup> sortedGroups = groups.stream()
                .sorted(Comparator.comparing(StudentGroup::getName))
                .toList();
        List<Subgroup> sortedSubgroups = subgroups.stream()
                .sorted(Comparator
                        .comparing((Subgroup subgroup) -> subgroup.getStudentGroup().getName())
                        .thenComparing(Subgroup::getName))
                .toList();

        return new RenderedLessonDto(
                rule.getId(),
                slot.getId(),
                date,
                date.getDayOfWeek().getValue(),
                dayName(date.getDayOfWeek()),
                weekNumber,
                dateParity,
                timeSlot.getId(),
                timeSlot.getOrderNumber(),
                timeSlot.getStartTime(),
                timeSlot.getEndTime(),
                rule.getSubject().getId(),
                rule.getSubject().getName(),
                slot.getTeacher().getId(),
                displayUserName(slot.getTeacher()),
                slot.getClassroom().getId(),
                slot.getClassroom().getName(),
                slot.getLessonType().getId(),
                slot.getLessonType().getLessonType(),
                slot.getLessonFormat(),
                sortedSubgroups.size() == 1 ? sortedSubgroups.get(0).getId() : null,
                sortedSubgroups.size() == 1 ? formatSubgroup(sortedSubgroups.get(0)) : null,
                sortedSubgroups.stream().map(Subgroup::getId).toList(),
                sortedSubgroups.stream().map(this::formatSubgroup).toList(),
                sortedGroups.stream().map(StudentGroup::getId).toList(),
                sortedGroups.stream().map(StudentGroup::getName).collect(Collectors.toList()),
                "Т",
                lessonTypeAcademicHours,
                consumedBeforeLesson,
                remainingAfterLesson,
                slot.getParity()
        );
    }

    private String formatSubgroup(Subgroup subgroup) {
        return subgroup.getStudentGroup().getName() + ": " + subgroup.getName();
    }

    private static final class TimeSlotSnapshot {

        private final Map<LocalDate, Long> dateScopeIds;
        private final Map<Integer, Long> weekdayScopeIds;
        private final Map<ScopeOrderKey, TimeSlot> slotsByScopeAndOrder;

        private TimeSlotSnapshot(Collection<TimeSlotDateAssignment> dateAssignments,
                                 Collection<TimeSlotScope> weekdayScopes,
                                 Collection<TimeSlot> timeSlots) {
            this.dateScopeIds = dateAssignments.stream()
                    .collect(Collectors.toUnmodifiableMap(
                            TimeSlotDateAssignment::getDate,
                            assignment -> assignment.getTimeSlotScope().getId()
                    ));
            this.weekdayScopeIds = weekdayScopes.stream()
                    .filter(scope -> scope.getDayOfWeek() != null)
                    .collect(Collectors.toUnmodifiableMap(
                            TimeSlotScope::getDayOfWeek,
                            TimeSlotScope::getId,
                            (first, second) -> first
                    ));
            this.slotsByScopeAndOrder = timeSlots.stream()
                    .collect(Collectors.toUnmodifiableMap(
                            slot -> new ScopeOrderKey(
                                    slot.getTimeSlotScope().getId(),
                                    slot.getOrderNumber()
                            ),
                            slot -> slot
                    ));
        }

        private static TimeSlotSnapshot empty() {
            return new TimeSlotSnapshot(List.of(), List.of(), List.of());
        }

        private TimeSlot resolve(TimeSlot baseSlot, LocalDate date) {
            if (baseSlot == null || baseSlot.getOrderNumber() == null || date == null) {
                return baseSlot;
            }
            Long scopeId = Optional.ofNullable(dateScopeIds.get(date))
                    .orElseGet(() -> weekdayScopeIds.get(date.getDayOfWeek().getValue()));
            if (scopeId == null) {
                return baseSlot;
            }
            return slotsByScopeAndOrder.getOrDefault(
                    new ScopeOrderKey(scopeId, baseSlot.getOrderNumber()),
                    baseSlot
            );
        }
    }

    public static String dayName(DayOfWeek dayOfWeek) {
        String value = dayOfWeek.getDisplayName(TextStyle.FULL, Locale.forLanguageTag("ru-RU"));
        return value.substring(0, 1).toUpperCase(Locale.forLanguageTag("ru-RU")) + value.substring(1);
    }

    public static String displayUserName(User user) {
        if (user.getFullName() != null && !user.getFullName().isBlank()) {
            return user.getFullName();
        }
        return user.getUsername();
    }
}
