package com.magistr.app.service;

import com.magistr.app.dto.RenderedLessonDto;
import com.magistr.app.model.ScheduleOverride;
import com.magistr.app.model.StudentGroup;
import com.magistr.app.repository.GroupRepository;
import com.magistr.app.repository.ScheduleOverrideRepository;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class ScheduleQueryService {

    private static final int MAX_GROUPS_WITHOUT_SCOPE = 50;
    private static final long MAX_RANGE_DAYS = 120;

    private final ScheduleGeneratorService scheduleGeneratorService;
    private final GroupRepository groupRepository;
    private final ScheduleOverrideRepository scheduleOverrideRepository;
    private final StudentGroupLifecycleService groupLifecycleService;

    public ScheduleQueryService(ScheduleGeneratorService scheduleGeneratorService,
                                GroupRepository groupRepository,
                                ScheduleOverrideRepository scheduleOverrideRepository,
                                StudentGroupLifecycleService groupLifecycleService) {
        this.scheduleGeneratorService = scheduleGeneratorService;
        this.groupRepository = groupRepository;
        this.scheduleOverrideRepository = scheduleOverrideRepository;
        this.groupLifecycleService = groupLifecycleService;
    }

    public List<RenderedLessonDto> search(Long groupId,
                                          Long teacherId,
                                          Long classroomId,
                                          Long departmentId,
                                          Long subjectId,
                                          Long lessonTypeId,
                                          Long timeSlotId,
                                          String parity,
                                          LocalDate startDate,
                                          LocalDate endDate) {
        validateRange(startDate, endDate);
        List<RenderedLessonDto> generatedLessons;
        if (groupId == null && departmentId == null && teacherId != null) {
            generatedLessons = scheduleGeneratorService.buildScheduleForTeacher(teacherId, startDate, endDate);
        } else {
            List<StudentGroup> groups = resolveGroups(groupId, departmentId, startDate, endDate);
            generatedLessons = groups.stream()
                    .flatMap(group -> scheduleGeneratorService.buildScheduleForGroup(group.getId(), startDate, endDate).stream())
                    .toList();
        }
        List<RenderedLessonDto> lessons = applyOverrides(generatedLessons, startDate, endDate).stream()
                .filter(lesson -> teacherId == null || Objects.equals(lesson.teacherId(), teacherId))
                .filter(lesson -> classroomId == null || Objects.equals(lesson.classroomId(), classroomId))
                .filter(lesson -> subjectId == null || Objects.equals(lesson.subjectId(), subjectId))
                .filter(lesson -> lessonTypeId == null || Objects.equals(lesson.lessonTypeId(), lessonTypeId))
                .filter(lesson -> timeSlotId == null || Objects.equals(lesson.timeSlotId(), timeSlotId))
                .filter(lesson -> parity == null || parity.isBlank() || lesson.parity().name().equalsIgnoreCase(parity))
                .toList();
        return deduplicate(lessons);
    }

    private List<RenderedLessonDto> applyOverrides(List<RenderedLessonDto> lessons, LocalDate startDate, LocalDate endDate) {
        Map<String, ScheduleOverride> overrides = scheduleOverrideRepository
                .findByLessonDateBetweenWithDetails(startDate, endDate)
                .stream()
                .collect(Collectors.toMap(
                        override -> overrideKey(override.getBaseRuleSlot().getId(), override.getLessonDate()),
                        override -> override,
                        (first, second) -> second
                ));
        if (overrides.isEmpty()) {
            return lessons;
        }
        return lessons.stream()
                .map(lesson -> {
                    ScheduleOverride override = overrides.get(overrideKey(lesson.scheduleRuleSlotId(), lesson.date()));
                    return override == null ? lesson : applyOverride(lesson, override);
                })
                .filter(Objects::nonNull)
                .toList();
    }

    private RenderedLessonDto applyOverride(RenderedLessonDto lesson, ScheduleOverride override) {
        if ("CANCEL".equals(override.getAction())) {
            return null;
        }
        Long timeSlotId = lesson.timeSlotId();
        Integer timeSlotOrder = lesson.timeSlotOrder();
        java.time.LocalTime startTime = lesson.startTime();
        java.time.LocalTime endTime = lesson.endTime();
        if (override.getNewTimeSlot() != null) {
            timeSlotId = override.getNewTimeSlot().getId();
            timeSlotOrder = override.getNewTimeSlot().getOrderNumber();
            startTime = override.getNewTimeSlot().getStartTime();
            endTime = override.getNewTimeSlot().getEndTime();
        }

        Long classroomId = override.getNewClassroom() == null ? lesson.classroomId() : override.getNewClassroom().getId();
        String classroomName = override.getNewClassroom() == null ? lesson.classroomName() : override.getNewClassroom().getName();
        Long teacherId = override.getNewTeacher() == null ? lesson.teacherId() : override.getNewTeacher().getId();
        String teacherName = override.getNewTeacher() == null ? lesson.teacherName() : ScheduleGeneratorService.displayUserName(override.getNewTeacher());
        String lessonFormat = override.getNewLessonFormat() == null ? lesson.lessonFormat() : override.getNewLessonFormat();

        return new RenderedLessonDto(
                lesson.scheduleRuleId(),
                lesson.scheduleRuleSlotId(),
                lesson.date(),
                lesson.dayOfWeek(),
                lesson.dayName(),
                lesson.weekNumber(),
                lesson.parity(),
                timeSlotId,
                timeSlotOrder,
                startTime,
                endTime,
                lesson.subjectId(),
                lesson.subjectName(),
                teacherId,
                teacherName,
                classroomId,
                classroomName,
                lesson.lessonTypeId(),
                lesson.lessonTypeName(),
                lessonFormat,
                lesson.subgroupId(),
                lesson.subgroupName(),
                lesson.subgroupIds(),
                lesson.subgroupNames(),
                lesson.groupIds(),
                lesson.groupNames(),
                lesson.activityType(),
                lesson.lessonTypeAcademicHours(),
                lesson.consumedLessonTypeAcademicHoursBeforeLesson(),
                lesson.remainingLessonTypeAcademicHoursAfterLesson(),
                lesson.ruleParity()
        );
    }

    private String overrideKey(Long scheduleRuleSlotId, LocalDate date) {
        return scheduleRuleSlotId + ":" + date;
    }

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

    private List<StudentGroup> resolveGroups(Long groupId, Long departmentId, LocalDate startDate, LocalDate endDate) {
        if (groupId != null) {
            return groupRepository.findById(groupId)
                    .filter(group -> groupLifecycleService.mayHaveScheduleInRange(group, startDate, endDate))
                    .map(List::of)
                    .orElse(List.of());
        }
        List<StudentGroup> groups;
        if (departmentId != null) {
            groups = groupRepository.findByDepartmentId(departmentId);
        } else {
            groups = groupRepository.findAll();
        }
        groups = groups.stream()
                .filter(group -> groupLifecycleService.mayHaveScheduleInRange(group, startDate, endDate))
                .toList();
        if (departmentId == null && groups.size() > MAX_GROUPS_WITHOUT_SCOPE) {
            throw new IllegalArgumentException("Уточните группу или кафедру: широкий поиск затрагивает "
                    + groups.size() + " активных групп, максимум " + MAX_GROUPS_WITHOUT_SCOPE);
        }
        return groups;
    }

    private List<RenderedLessonDto> deduplicate(List<RenderedLessonDto> lessons) {
        Map<String, MutableLesson> unique = new LinkedHashMap<>();
        for (RenderedLessonDto lesson : lessons) {
            String key = lessonKey(lesson);
            unique.computeIfAbsent(key, ignored -> new MutableLesson(lesson)).merge(lesson);
        }
        return unique.values().stream()
                .map(MutableLesson::toDto)
                .sorted(Comparator
                        .comparing(RenderedLessonDto::date)
                        .thenComparing(RenderedLessonDto::timeSlotOrder, Comparator.nullsLast(Integer::compareTo))
                        .thenComparing(RenderedLessonDto::subjectName, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private String lessonKey(RenderedLessonDto lesson) {
        return String.join(":",
                String.valueOf(lesson.date()),
                String.valueOf(lesson.scheduleRuleSlotId()),
                String.valueOf(lesson.classroomId()),
                String.valueOf(lesson.timeSlotOrder()),
                String.valueOf(lesson.teacherId()),
                String.valueOf(lesson.subjectId()),
                sortedSignature(lesson.subgroupIds())
        );
    }

    private String sortedSignature(List<Long> values) {
        if (values == null || values.isEmpty()) {
            return "";
        }
        return values.stream()
                .map(String::valueOf)
                .sorted()
                .collect(Collectors.joining(","));
    }

    private static class MutableLesson {
        private final RenderedLessonDto source;
        private final LinkedHashSet<Long> groupIds = new LinkedHashSet<>();
        private final LinkedHashSet<String> groupNames = new LinkedHashSet<>();
        private final LinkedHashSet<Long> subgroupIds = new LinkedHashSet<>();
        private final LinkedHashSet<String> subgroupNames = new LinkedHashSet<>();

        MutableLesson(RenderedLessonDto source) {
            this.source = source;
        }

        void merge(RenderedLessonDto lesson) {
            if (lesson.groupIds() != null) {
                groupIds.addAll(lesson.groupIds());
            }
            if (lesson.groupNames() != null) {
                groupNames.addAll(lesson.groupNames());
            }
            if (lesson.subgroupIds() != null) {
                subgroupIds.addAll(lesson.subgroupIds());
            }
            if (lesson.subgroupNames() != null) {
                subgroupNames.addAll(lesson.subgroupNames());
            }
        }

        RenderedLessonDto toDto() {
            return new RenderedLessonDto(
                    source.scheduleRuleId(),
                    source.scheduleRuleSlotId(),
                    source.date(),
                    source.dayOfWeek(),
                    source.dayName(),
                    source.weekNumber(),
                    source.parity(),
                    source.timeSlotId(),
                    source.timeSlotOrder(),
                    source.startTime(),
                    source.endTime(),
                    source.subjectId(),
                    source.subjectName(),
                    source.teacherId(),
                    source.teacherName(),
                    source.classroomId(),
                    source.classroomName(),
                    source.lessonTypeId(),
                    source.lessonTypeName(),
                    source.lessonFormat(),
                    subgroupIds.size() == 1 ? subgroupIds.iterator().next() : null,
                    subgroupNames.size() == 1 ? subgroupNames.iterator().next() : null,
                    List.copyOf(subgroupIds),
                    List.copyOf(subgroupNames),
                    List.copyOf(groupIds),
                    List.copyOf(groupNames),
                    source.activityType(),
                    source.lessonTypeAcademicHours(),
                    source.consumedLessonTypeAcademicHoursBeforeLesson(),
                    source.remainingLessonTypeAcademicHoursAfterLesson(),
                    source.ruleParity()
            );
        }
    }
}
