package com.magistr.app.controller;

import com.magistr.app.config.auth.RequireRoles;
import com.magistr.app.dto.ScheduleRuleDto;
import com.magistr.app.dto.ScheduleRuleSlotDto;
import com.magistr.app.model.*;
import com.magistr.app.repository.*;
import com.magistr.app.service.ScheduleGeneratorService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;

import java.time.DayOfWeek;
import java.time.temporal.ChronoUnit;
import java.util.*;

@RestController
@RequestMapping("/api/admin/schedule-rules")
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
public class ScheduleRuleAdminController {

    private static final String APPLY_MODE_DEFAULT = "DEFAULT";
    private static final int ACADEMIC_HOURS_PER_SLOT = 2;
    private static final Set<ScheduleParity> VALID_SLOT_PARITIES = Set.of(
            ScheduleParity.BOTH,
            ScheduleParity.ODD,
            ScheduleParity.EVEN
    );

    private final ScheduleRuleRepository scheduleRuleRepository;
    private final SubjectRepository subjectRepository;
    private final SemesterRepository semesterRepository;
    private final GroupRepository groupRepository;
    private final TimeSlotRepository timeSlotRepository;
    private final UserRepository userRepository;
    private final ClassroomRepository classroomRepository;
    private final LessonTypesRepository lessonTypesRepository;
    private final SubgroupRepository subgroupRepository;
    private final ScheduleGeneratorService scheduleGeneratorService;

    public ScheduleRuleAdminController(ScheduleRuleRepository scheduleRuleRepository,
                                       SubjectRepository subjectRepository,
                                       SemesterRepository semesterRepository,
                                       GroupRepository groupRepository,
                                       TimeSlotRepository timeSlotRepository,
                                       UserRepository userRepository,
                                       ClassroomRepository classroomRepository,
                                       LessonTypesRepository lessonTypesRepository,
                                       SubgroupRepository subgroupRepository,
                                       ScheduleGeneratorService scheduleGeneratorService) {
        this.scheduleRuleRepository = scheduleRuleRepository;
        this.subjectRepository = subjectRepository;
        this.semesterRepository = semesterRepository;
        this.groupRepository = groupRepository;
        this.timeSlotRepository = timeSlotRepository;
        this.userRepository = userRepository;
        this.classroomRepository = classroomRepository;
        this.lessonTypesRepository = lessonTypesRepository;
        this.subgroupRepository = subgroupRepository;
        this.scheduleGeneratorService = scheduleGeneratorService;
    }

    @GetMapping
    public List<ScheduleRuleDto> getAll(@RequestParam(required = false) Long semesterId,
                                        @RequestParam(required = false) Long groupId) {
        return scheduleRuleRepository.findAllWithDetails().stream()
                .filter(rule -> semesterId == null || Objects.equals(rule.getSemester().getId(), semesterId))
                .filter(rule -> groupId == null || rule.getGroups().stream().anyMatch(group -> Objects.equals(group.getId(), groupId)))
                .map(this::toDto)
                .toList();
    }

    @GetMapping("/{id}")
    public ResponseEntity<?> getById(@PathVariable Long id) {
        return scheduleRuleRepository.findByIdWithDetails(id)
                .<ResponseEntity<?>>map(rule -> ResponseEntity.ok(toDto(rule)))
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PostMapping
    @Transactional
    public ResponseEntity<?> create(@RequestBody ScheduleRuleDto request) {
        String validationError = validateRule(request);
        if (validationError != null) {
            return ResponseEntity.badRequest().body(Map.of("message", validationError));
        }

        try {
            ScheduleRule rule = new ScheduleRule();
            applyRule(rule, request);
            ScheduleRuleConflict conflict = findConflict(request, rule.getSlots(), null);
            if (conflict != null) {
                return conflictResponse(conflict);
            }
            ScheduleRule saved = scheduleRuleRepository.save(rule);
            scheduleGeneratorService.clearCache();
            return ResponseEntity.ok(toDto(saved));
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
        }
    }

    @PutMapping("/{id}")
    @Transactional
    public ResponseEntity<?> update(@PathVariable Long id, @RequestBody ScheduleRuleDto request) {
        ScheduleRule rule = scheduleRuleRepository.findByIdWithDetails(id).orElse(null);
        if (rule == null) {
            return ResponseEntity.notFound().build();
        }

        String validationError = validateRule(request);
        if (validationError != null) {
            return ResponseEntity.badRequest().body(Map.of("message", validationError));
        }

        try {
            ScheduleRule candidate = new ScheduleRule();
            applyRule(candidate, request);
            ScheduleRuleConflict conflict = findConflict(request, candidate.getSlots(), rule.getId());
            if (conflict != null) {
                return conflictResponse(conflict);
            }
            applyRule(rule, request);
            ScheduleRule saved = scheduleRuleRepository.save(rule);
            scheduleGeneratorService.clearCache();
            return ResponseEntity.ok(toDto(saved));
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
        }
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<?> delete(@PathVariable Long id) {
        ScheduleRule rule = scheduleRuleRepository.findById(id).orElse(null);
        if (rule == null) {
            return ResponseEntity.notFound().build();
        }
        rule.setStatus(LifecycleEntity.STATUS_ARCHIVED);
        scheduleRuleRepository.save(rule);
        scheduleGeneratorService.clearCache();
        return ResponseEntity.ok(Map.of("message", "Правило расписания архивировано"));
    }

    private String validateRule(ScheduleRuleDto request) {
        if (request.subjectId() == null) {
            return "Дисциплина обязательна";
        }
        if (request.semesterId() == null) {
            return "Семестр обязателен";
        }
        String hoursValidationError = validateTypeHours(request);
        if (hoursValidationError != null) {
            return hoursValidationError;
        }
        if (request.groupIds() == null || request.groupIds().isEmpty()) {
            return "Нужно выбрать хотя бы одну группу";
        }
        if (request.slots() == null || request.slots().isEmpty()) {
            return "Добавьте хотя бы один слот занятия";
        }
        return null;
    }

    private void applyRule(ScheduleRule rule, ScheduleRuleDto request) {
        Subject subject = subjectRepository.findById(request.subjectId())
                .orElseThrow(() -> new IllegalArgumentException("Дисциплина не найдена"));
        if (subject.isArchivedRecord()) {
            throw new IllegalArgumentException("Архивную дисциплину нельзя использовать в новых правилах");
        }
        Semester semester = semesterRepository.findById(request.semesterId())
                .orElseThrow(() -> new IllegalArgumentException("Семестр не найден"));
        List<StudentGroup> groups = groupRepository.findAllById(request.groupIds());
        if (groups.size() != new HashSet<>(request.groupIds()).size()) {
            throw new IllegalArgumentException("Одна или несколько групп не найдены");
        }
        if (groups.stream().anyMatch(StudentGroup::isArchivedRecord)) {
            throw new IllegalArgumentException("Архивные группы нельзя использовать в новых правилах");
        }

        rule.setSubject(subject);
        rule.setSemester(semester);
        rule.setLectureAcademicHours(request.lectureAcademicHours());
        rule.setLaboratoryAcademicHours(request.laboratoryAcademicHours());
        rule.setPracticeAcademicHours(request.practiceAcademicHours());
        rule.setLectureStartWeek(request.lectureStartWeek());
        rule.setLaboratoryStartWeek(request.laboratoryStartWeek());
        rule.setPracticeStartWeek(request.practiceStartWeek());
        rule.getGroups().clear();
        rule.getGroups().addAll(groups);

        List<ScheduleRuleSlot> slots = new ArrayList<>();
        for (ScheduleRuleSlotDto slotDto : request.slots()) {
            slots.add(buildSlot(rule, slotDto));
        }
        validateTypeCoverage(rule, slots);

        rule.getSlots().clear();
        rule.getSlots().addAll(slots);
    }

    private ResponseEntity<?> conflictResponse(ScheduleRuleConflict conflict) {
        return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of(
                "message", "Невозможно сохранить правило: слот занят",
                "conflictRule", toDto(conflict.rule()),
                "conflictFields", conflict.fields().stream().toList(),
                "conflictReasons", conflict.fields().stream().map(this::conflictFieldLabel).toList()
        ));
    }

    private String conflictFieldLabel(String field) {
        return switch (field) {
            case "teacher" -> "Преподаватель";
            case "classroom" -> "Аудитория";
            case "group" -> "Группа";
            default -> field;
        };
    }

    private ScheduleRuleConflict findConflict(ScheduleRuleDto request, Collection<ScheduleRuleSlot> newSlots, Long excludeRuleId) {
        Map<ScheduleRuleSlot, Set<Integer>> newActiveWeeks = activeWeeksBySlot(newSlots);
        return scheduleRuleRepository.findAllWithDetails().stream()
                .filter(rule -> !Objects.equals(rule.getId(), excludeRuleId))
                .filter(rule -> rule.getSemester() != null && Objects.equals(rule.getSemester().getId(), request.semesterId()))
                .map(rule -> toConflict(rule, newSlots, newActiveWeeks, rule.getSlots()))
                .filter(Objects::nonNull)
                .min(Comparator.comparing(conflict -> conflict.rule().getId()))
                .orElse(null);
    }

    private ScheduleRuleConflict toConflict(ScheduleRule existingRule,
                                            Collection<ScheduleRuleSlot> newSlots,
                                            Map<ScheduleRuleSlot, Set<Integer>> newActiveWeeks,
                                            Collection<ScheduleRuleSlot> existingSlots) {
        Map<ScheduleRuleSlot, Set<Integer>> existingActiveWeeks = activeWeeksBySlot(existingSlots);
        LinkedHashSet<String> fields = new LinkedHashSet<>();
        for (ScheduleRuleSlot newSlot : newSlots) {
            for (ScheduleRuleSlot existingSlot : existingSlots) {
                if (sameTimeSlot(newSlot, existingSlot)
                        && activeWeeksOverlap(newActiveWeeks.get(newSlot), existingActiveWeeks.get(existingSlot))) {
                    fields.addAll(conflictFields(newSlot, existingSlot));
                }
            }
        }
        return fields.isEmpty() ? null : new ScheduleRuleConflict(existingRule, fields);
    }

    private Set<String> conflictFields(ScheduleRuleSlot first, ScheduleRuleSlot second) {
        LinkedHashSet<String> fields = new LinkedHashSet<>();
        if (Objects.equals(teacherId(first), teacherId(second))) {
            fields.add("teacher");
        }
        if (Objects.equals(classroomId(first), classroomId(second))) {
            fields.add("classroom");
        }
        if (sameAudience(first, second)) {
            fields.add("group");
        }
        return fields;
    }

    private boolean sameTimeSlot(ScheduleRuleSlot first, ScheduleRuleSlot second) {
        return Objects.equals(first.getDayOfWeek(), second.getDayOfWeek())
                && parityOverlaps(first.getParity(), second.getParity())
                && Objects.equals(timeSlotId(first), timeSlotId(second));
    }

    private Long teacherId(ScheduleRuleSlot slot) {
        return slot.getTeacher() == null ? null : slot.getTeacher().getId();
    }

    private Long classroomId(ScheduleRuleSlot slot) {
        return slot.getClassroom() == null ? null : slot.getClassroom().getId();
    }

    private Long timeSlotId(ScheduleRuleSlot slot) {
        return slot.getTimeSlot() == null ? null : slot.getTimeSlot().getId();
    }

    private boolean parityOverlaps(ScheduleParity first, ScheduleParity second) {
        if (Objects.equals(first, second)) {
            return true;
        }
        return first == ScheduleParity.BOTH || second == ScheduleParity.BOTH;
    }

    private boolean sameAudience(ScheduleRuleSlot first, ScheduleRuleSlot second) {
        Set<Long> firstGroupIds = groupIds(first);
        Set<Long> secondGroupIds = groupIds(second);
        firstGroupIds.retainAll(secondGroupIds);

        for (Long groupId : firstGroupIds) {
            if (slotAppliesToWholeGroup(first, groupId) || slotAppliesToWholeGroup(second, groupId)) {
                return true;
            }
            Set<Long> firstSubgroupIds = subgroupIdsForGroup(first, groupId);
            Set<Long> secondSubgroupIds = subgroupIdsForGroup(second, groupId);
            firstSubgroupIds.retainAll(secondSubgroupIds);
            if (!firstSubgroupIds.isEmpty()) {
                return true;
            }
        }
        return false;
    }

    private Set<Long> groupIds(ScheduleRuleSlot slot) {
        Set<Long> ids = new HashSet<>();
        if (slot.getScheduleRule() == null) {
            return ids;
        }
        for (StudentGroup group : slot.getScheduleRule().getGroups()) {
            ids.add(group.getId());
        }
        return ids;
    }

    private boolean slotAppliesToWholeGroup(ScheduleRuleSlot slot, Long groupId) {
        return subgroupIdsForGroup(slot, groupId).isEmpty();
    }

    private Set<Long> subgroupIdsForGroup(ScheduleRuleSlot slot, Long groupId) {
        Set<Long> ids = new HashSet<>();
        for (Subgroup subgroup : slot.getSubgroups()) {
            if (subgroup.getStudentGroup() != null
                    && Objects.equals(subgroup.getStudentGroup().getId(), groupId)) {
                ids.add(subgroup.getId());
            }
        }
        return ids;
    }

    private Map<ScheduleRuleSlot, Set<Integer>> activeWeeksBySlot(Collection<ScheduleRuleSlot> slots) {
        Map<ScheduleRuleSlot, Set<Integer>> activeWeeks = new IdentityHashMap<>();
        slots.forEach(slot -> activeWeeks.put(slot, new HashSet<>()));
        if (slots.isEmpty()) {
            return activeWeeks;
        }

        ScheduleRule rule = slots.iterator().next().getScheduleRule();
        if (rule == null || rule.getSemester() == null) {
            return activeWeeks;
        }

        int maxWeeks = maxWeeksForRule(rule);
        for (ScheduleLessonCategory category : ScheduleLessonCategory.values()) {
            int totalHours = rule.academicHoursFor(category);
            if (totalHours <= 0) {
                continue;
            }

            List<ScheduleRuleSlot> categorySlots = slots.stream()
                    .filter(slot -> ScheduleLessonCategory.fromLessonType(slot.getLessonType()) == category)
                    .sorted(this::compareSlotsForGeneration)
                    .toList();
            if (categorySlots.isEmpty()) {
                continue;
            }

            Map<String, Integer> consumedHours = new HashMap<>();
            int startWeek = Math.max(1, rule.startWeekFor(category));
            for (int week = startWeek; week <= maxWeeks; week++) {
                for (ScheduleRuleSlot slot : categorySlots) {
                    if (!slotAppliesToWeek(slot, week)) {
                        continue;
                    }

                    for (String scopeKey : consumptionScopes(slot, category)) {
                        int consumed = consumedHours.getOrDefault(scopeKey, 0);
                        if (consumed >= totalHours) {
                            continue;
                        }
                        activeWeeks.get(slot).add(week);
                        consumedHours.put(scopeKey, consumed + ACADEMIC_HOURS_PER_SLOT);
                    }
                }
            }
        }
        return activeWeeks;
    }

    private boolean activeWeeksOverlap(Set<Integer> first, Set<Integer> second) {
        if (first == null || second == null || first.isEmpty() || second.isEmpty()) {
            return false;
        }
        Set<Integer> weeks = new HashSet<>(first);
        weeks.retainAll(second);
        return !weeks.isEmpty();
    }

    private int maxWeeksForRule(ScheduleRule rule) {
        Semester semester = rule.getSemester();
        if (semester != null && semester.getStartDate() != null && semester.getEndDate() != null) {
            long days = Math.max(1, ChronoUnit.DAYS.between(semester.getStartDate(), semester.getEndDate()) + 1);
            return Math.max(1, (int) Math.ceil(days / 7.0));
        }
        return 18;
    }

    private int compareSlotsForGeneration(ScheduleRuleSlot first, ScheduleRuleSlot second) {
        int dayCompare = Integer.compare(
                first.getDayOfWeek() == null ? 0 : first.getDayOfWeek(),
                second.getDayOfWeek() == null ? 0 : second.getDayOfWeek()
        );
        if (dayCompare != 0) {
            return dayCompare;
        }
        int orderCompare = Integer.compare(
                first.getTimeSlot() == null || first.getTimeSlot().getOrderNumber() == null ? 0 : first.getTimeSlot().getOrderNumber(),
                second.getTimeSlot() == null || second.getTimeSlot().getOrderNumber() == null ? 0 : second.getTimeSlot().getOrderNumber()
        );
        if (orderCompare != 0) {
            return orderCompare;
        }
        return Long.compare(first.getId() == null ? 0L : first.getId(), second.getId() == null ? 0L : second.getId());
    }

    private boolean slotAppliesToWeek(ScheduleRuleSlot slot, int week) {
        ScheduleParity parity = slot.getParity();
        if (parity == null || parity == ScheduleParity.BOTH) {
            return true;
        }
        ScheduleParity weekParity = week % 2 == 0 ? ScheduleParity.EVEN : ScheduleParity.ODD;
        return parity == weekParity;
    }

    private List<String> consumptionScopes(ScheduleRuleSlot slot, ScheduleLessonCategory category) {
        if (category == ScheduleLessonCategory.LABORATORY && !slot.getSubgroups().isEmpty()) {
            return slot.getSubgroups().stream()
                    .map(subgroup -> "subgroup:" + subgroup.getId())
                    .toList();
        }
        if (slot.getScheduleRule() == null || slot.getScheduleRule().getGroups().isEmpty()) {
            return List.of("rule");
        }
        return slot.getScheduleRule().getGroups().stream()
                .map(group -> "group:" + group.getId())
                .toList();
    }

    private ScheduleRuleSlot buildSlot(ScheduleRule rule, ScheduleRuleSlotDto slotDto) {
        if (slotDto.dayOfWeek() == null || slotDto.dayOfWeek() < 1 || slotDto.dayOfWeek() > 7) {
            throw new IllegalArgumentException("День недели должен быть от 1 до 7");
        }
        if (slotDto.parity() == null || !VALID_SLOT_PARITIES.contains(slotDto.parity())) {
            throw new IllegalArgumentException("Чётность недели должна быть: каждая, нечётная или чётная");
        }
        TimeSlot timeSlot = timeSlotRepository.findById(slotDto.timeSlotId())
                .orElseThrow(() -> new IllegalArgumentException("Временной слот не найден"));
        if (timeSlot.getTimeSlotScope() == null
                || !APPLY_MODE_DEFAULT.equals(timeSlot.getTimeSlotScope().getApplyMode())) {
            throw new IllegalArgumentException("В правилах расписания можно выбирать только базовые временные слоты");
        }
        User teacher = userRepository.findById(slotDto.teacherId())
                .orElseThrow(() -> new IllegalArgumentException("Преподаватель не найден"));
        if (teacher.isArchivedRecord()) {
            throw new IllegalArgumentException("Архивного преподавателя нельзя назначать в расписание");
        }
        Classroom classroom = classroomRepository.findById(slotDto.classroomId())
                .orElseThrow(() -> new IllegalArgumentException("Аудитория не найдена"));
        if (classroom.isArchivedRecord() || Boolean.FALSE.equals(classroom.getIsAvailable())) {
            throw new IllegalArgumentException("Аудитория недоступна для новых назначений");
        }
        LessonType lessonType = lessonTypesRepository.findById(slotDto.lessonTypeId())
                .orElseThrow(() -> new IllegalArgumentException("Тип занятия не найден"));
        ScheduleLessonCategory category = ScheduleLessonCategory.fromLessonType(lessonType);
        Set<Subgroup> subgroups = resolveSubgroups(rule, slotDto, category);
        if (slotDto.lessonFormat() == null || slotDto.lessonFormat().isBlank()) {
            throw new IllegalArgumentException("Формат занятия обязателен");
        }

        ScheduleRuleSlot slot = new ScheduleRuleSlot();
        slot.setScheduleRule(rule);
        slot.setDayOfWeek(slotDto.dayOfWeek());
        slot.setParity(slotDto.parity());
        slot.setTimeSlot(timeSlot);
        slot.setSubgroups(subgroups);
        slot.setTeacher(teacher);
        slot.setClassroom(classroom);
        slot.setLessonType(lessonType);
        slot.setLessonFormat(slotDto.lessonFormat().trim());
        return slot;
    }

    private Set<Subgroup> resolveSubgroups(ScheduleRule rule, ScheduleRuleSlotDto slotDto, ScheduleLessonCategory category) {
        LinkedHashSet<Long> subgroupIds = new LinkedHashSet<>();
        if (slotDto.subgroupIds() != null) {
            slotDto.subgroupIds().stream()
                    .filter(Objects::nonNull)
                    .forEach(subgroupIds::add);
        }
        if (slotDto.subgroupId() != null) {
            subgroupIds.add(slotDto.subgroupId());
        }
        if (subgroupIds.isEmpty()) {
            return new LinkedHashSet<>();
        }

        if (category != ScheduleLessonCategory.LABORATORY) {
            throw new IllegalArgumentException("Подгруппы можно выбирать только для лабораторных занятий");
        }

        Set<Long> ruleGroupIds = rule.getGroups().stream()
                .map(StudentGroup::getId)
                .collect(java.util.stream.Collectors.toSet());
        Set<Long> subgroupGroupIds = new HashSet<>();
        LinkedHashSet<Subgroup> subgroups = new LinkedHashSet<>();

        for (Long subgroupId : subgroupIds) {
            Subgroup subgroup = subgroupRepository.findById(subgroupId)
                    .orElseThrow(() -> new IllegalArgumentException("Подгруппа не найдена"));
            Long subgroupGroupId = subgroup.getStudentGroup().getId();
            if (!ruleGroupIds.contains(subgroupGroupId)) {
                throw new IllegalArgumentException("Подгруппа должна относиться к одной из групп правила");
            }
            if (!subgroupGroupIds.add(subgroupGroupId)) {
                throw new IllegalArgumentException("В одном слоте можно выбрать не больше одной подгруппы каждой группы");
            }
            subgroups.add(subgroup);
        }

        return subgroups;
    }

    private String validateTypeHours(ScheduleRuleDto request) {
        if (isMissing(request.lectureAcademicHours())
                || isMissing(request.laboratoryAcademicHours())
                || isMissing(request.practiceAcademicHours())) {
            return "Укажите количество часов для всех типов занятий";
        }
        if (isNegative(request.lectureAcademicHours())
                || isNegative(request.laboratoryAcademicHours())
                || isNegative(request.practiceAcademicHours())) {
            return "Количество часов по типам занятий не может быть отрицательным";
        }
        if (isInvalidStartWeek(request.lectureStartWeek())
                || isInvalidStartWeek(request.laboratoryStartWeek())
                || isInvalidStartWeek(request.practiceStartWeek())) {
            return "Неделя начала занятий должна быть больше нуля";
        }
        int totalHours = valueOrZero(request.lectureAcademicHours())
                + valueOrZero(request.laboratoryAcademicHours())
                + valueOrZero(request.practiceAcademicHours());
        if (totalHours <= 0) {
            return "Укажите часы хотя бы для одного типа занятий";
        }
        return null;
    }

    private void validateTypeCoverage(ScheduleRule rule, List<ScheduleRuleSlot> slots) {
        EnumSet<ScheduleLessonCategory> categoriesWithSlots = EnumSet.noneOf(ScheduleLessonCategory.class);
        for (ScheduleRuleSlot slot : slots) {
            categoriesWithSlots.add(ScheduleLessonCategory.fromLessonType(slot.getLessonType()));
        }

        for (ScheduleLessonCategory category : ScheduleLessonCategory.values()) {
            int hours = rule.academicHoursFor(category);
            boolean hasSlots = categoriesWithSlots.contains(category);
            if (hours > 0 && !hasSlots) {
                throw new IllegalArgumentException("Для типа \"" + category.getDisplayName() + "\" добавьте хотя бы один слот");
            }
            if (hours == 0 && hasSlots) {
                throw new IllegalArgumentException("Для типа \"" + category.getDisplayName() + "\" укажите часы или удалите слоты этого типа");
            }
        }
    }

    private boolean isMissing(Integer value) {
        return value == null;
    }

    private boolean isNegative(Integer value) {
        return value < 0;
    }

    private boolean isInvalidStartWeek(Integer value) {
        return value == null || value <= 0;
    }

    private int valueOrZero(Integer value) {
        return value == null ? 0 : value;
    }

    private ScheduleRuleDto toDto(ScheduleRule rule) {
        List<StudentGroup> groups = rule.getGroups().stream()
                .sorted(Comparator.comparing(StudentGroup::getName))
                .toList();
        List<ScheduleRuleSlotDto> slots = rule.getSlots().stream()
                .sorted(Comparator
                        .comparing(ScheduleRuleSlot::getDayOfWeek)
                        .thenComparing(slot -> slot.getTimeSlot().getOrderNumber())
                        .thenComparing(ScheduleRuleSlot::getId))
                .map(this::toSlotDto)
                .toList();

        return new ScheduleRuleDto(
                rule.getId(),
                rule.getSubject().getId(),
                rule.getSubject().getName(),
                rule.getSemester().getId(),
                rule.getSemester().getAcademicYear().getTitle(),
                rule.getSemester().getSemesterType(),
                rule.getLectureAcademicHours(),
                rule.getLaboratoryAcademicHours(),
                rule.getPracticeAcademicHours(),
                rule.getLectureStartWeek(),
                rule.getLaboratoryStartWeek(),
                rule.getPracticeStartWeek(),
                groups.stream().map(StudentGroup::getId).toList(),
                groups.stream().map(StudentGroup::getName).toList(),
                slots
        );
    }

    private ScheduleRuleSlotDto toSlotDto(ScheduleRuleSlot slot) {
        TimeSlot timeSlot = slot.getTimeSlot();
        List<Subgroup> sortedSubgroups = sortedSubgroups(slot);
        return new ScheduleRuleSlotDto(
                slot.getId(),
                slot.getDayOfWeek(),
                dayName(slot.getDayOfWeek()),
                slot.getParity(),
                timeSlot.getId(),
                timeSlot.getOrderNumber(),
                formatTimeSlot(timeSlot),
                sortedSubgroups.isEmpty() ? null : sortedSubgroups.get(0).getId(),
                sortedSubgroups.isEmpty() ? null : formatSubgroup(sortedSubgroups.get(0)),
                sortedSubgroups.stream().map(Subgroup::getId).toList(),
                sortedSubgroups.stream().map(this::formatSubgroup).toList(),
                slot.getTeacher().getId(),
                ScheduleGeneratorService.displayUserName(slot.getTeacher()),
                slot.getClassroom().getId(),
                slot.getClassroom().getName(),
                slot.getLessonType().getId(),
                slot.getLessonType().getLessonType(),
                slot.getLessonFormat()
        );
    }

    private String dayName(Integer dayOfWeek) {
        if (dayOfWeek == null || dayOfWeek < 1 || dayOfWeek > 7) {
            return "Неизвестно";
        }
        return ScheduleGeneratorService.dayName(DayOfWeek.of(dayOfWeek));
    }

    private String formatTimeSlot(TimeSlot timeSlot) {
        return timeSlot.getStartTime() + " - " + timeSlot.getEndTime();
    }

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

    private List<Subgroup> sortedSubgroups(ScheduleRuleSlot slot) {
        return slot.getSubgroups().stream()
                .sorted(Comparator
                        .comparing((Subgroup subgroup) -> subgroup.getStudentGroup().getName())
                        .thenComparing(Subgroup::getName))
                .toList();
    }

    private record ScheduleRuleConflict(ScheduleRule rule, LinkedHashSet<String> fields) {
    }
}
