package com.magistr.app.service;

import com.magistr.app.model.*;
import com.magistr.app.repository.AcademicCalendarDayRepository;
import com.magistr.app.repository.SemesterRepository;
import com.magistr.app.repository.StudentGroupCalendarAssignmentRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

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

@Service
public class AcademicDateService {

    private final SemesterRepository semesterRepository;
    private final AcademicCalendarDayRepository calendarDayRepository;
    private final StudentGroupCalendarAssignmentRepository assignmentRepository;
    private final boolean evenWeekIsUpper;

    public AcademicDateService(SemesterRepository semesterRepository,
                               AcademicCalendarDayRepository calendarDayRepository,
                               StudentGroupCalendarAssignmentRepository assignmentRepository,
                               @Value("${app.schedule.even-week-is-upper:false}") boolean evenWeekIsUpper) {
        this.semesterRepository = semesterRepository;
        this.calendarDayRepository = calendarDayRepository;
        this.assignmentRepository = assignmentRepository;
        this.evenWeekIsUpper = evenWeekIsUpper;
    }

    public Optional<Semester> findSemester(LocalDate date) {
        return semesterRepository.findFirstByStartDateLessThanEqualAndEndDateGreaterThanEqual(date, date);
    }

    public List<Semester> findSemesters(LocalDate startDate, LocalDate endDate) {
        if (startDate == null || endDate == null || endDate.isBefore(startDate)) {
            return List.of();
        }
        return semesterRepository.findOverlappingForSchedule(startDate, endDate);
    }

    /**
     * Загружает календарные данные одним набором batch-запросов и возвращает снимок,
     * который не обращается к репозиториям во внутренних циклах генератора.
     */
    public ScheduleSnapshot createScheduleSnapshot(Collection<StudentGroup> groups,
                                                   Collection<Semester> semesters,
                                                   LocalDate endDate) {
        List<Semester> semesterSnapshot = Optional.ofNullable(semesters)
                .orElseGet(List::of)
                .stream()
                .filter(Objects::nonNull)
                .sorted(Comparator.comparing(Semester::getStartDate).thenComparing(Semester::getId))
                .toList();
        LocalDate snapshotStart = semesterSnapshot.stream()
                .map(Semester::getStartDate)
                .min(LocalDate::compareTo)
                .orElse(endDate);

        Set<Long> groupIds = Optional.ofNullable(groups)
                .orElseGet(List::of)
                .stream()
                .filter(Objects::nonNull)
                .map(StudentGroup::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        Set<Long> academicYearIds = semesterSnapshot.stream()
                .map(Semester::getAcademicYear)
                .filter(Objects::nonNull)
                .map(AcademicYear::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));

        List<StudentGroupCalendarAssignment> assignments = groupIds.isEmpty() || academicYearIds.isEmpty()
                ? List.of()
                : assignmentRepository.findForScheduleBatch(groupIds, academicYearIds);
        Set<Long> calendarIds = assignments.stream()
                .map(StudentGroupCalendarAssignment::getAcademicCalendar)
                .filter(Objects::nonNull)
                .map(AcademicCalendar::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        List<AcademicCalendarDay> calendarDays = calendarIds.isEmpty()
                || snapshotStart == null
                || endDate == null
                || endDate.isBefore(snapshotStart)
                ? List.of()
                : calendarDayRepository.findForScheduleBatch(calendarIds, snapshotStart, endDate);

        return new ScheduleSnapshot(semesterSnapshot, assignments, calendarDays, snapshotStart, endDate);
    }

    public int getWeekNumber(Semester semester, LocalDate date) {
        long days = ChronoUnit.DAYS.between(semester.getStartDate(), date);
        if (days < 0) {
            return 0;
        }
        return (int) (days / 7) + 1;
    }

    public ScheduleParity getParity(Semester semester, LocalDate date) {
        int weekNumber = getWeekNumber(semester, date);
        boolean evenWeek = weekNumber % 2 == 0;
        if (evenWeekIsUpper) {
            return evenWeek ? ScheduleParity.ODD : ScheduleParity.EVEN;
        }
        return evenWeek ? ScheduleParity.EVEN : ScheduleParity.ODD;
    }

    public int getCourseNumber(StudentGroup group, LocalDate date) {
        int academicYearStart = date.getMonthValue() >= 9 ? date.getYear() : date.getYear() - 1;
        return academicYearStart - group.getYearStartStudy() + 1;
    }

    public Optional<AcademicCalendarActivityType> getActivityType(StudentGroup group, Semester semester, LocalDate date) {
        if (group == null || semester == null || semester.getAcademicYear() == null || date == null) {
            return Optional.empty();
        }

        int courseNumber = getCourseNumber(group, date);
        if (courseNumber < 1) {
            return Optional.empty();
        }

        return assignmentRepository.findForSchedule(group.getId(), semester.getAcademicYear().getId())
                .flatMap(assignment -> calendarDayRepository.findByCalendarIdAndCourseNumberAndDate(
                        assignment.getAcademicCalendar().getId(),
                        courseNumber,
                        date
                ))
                .map(AcademicCalendarDay::getActivityType);
    }

    public boolean isTheoryDay(StudentGroup group, Semester semester, LocalDate date) {
        return getActivityType(group, semester, date)
                .map(AcademicCalendarActivityType::getAllowSchedule)
                .orElse(false);
    }

    public boolean hasCalendarAssignment(StudentGroup group, Semester semester) {
        if (group == null || semester == null || semester.getAcademicYear() == null) {
            return false;
        }
        return assignmentRepository.findForSchedule(group.getId(), semester.getAcademicYear().getId()).isPresent();
    }

    public static final class ScheduleSnapshot {

        private final Map<LocalDate, Semester> semesterByDate;
        private final Map<GroupAcademicYearKey, StudentGroupCalendarAssignment> assignmentByGroupAndYear;
        private final Map<CalendarDayKey, AcademicCalendarActivityType> activityByCalendarCourseAndDate;
        private final LocalDate startDate;
        private final LocalDate endDate;

        private ScheduleSnapshot(Collection<Semester> semesters,
                                 Collection<StudentGroupCalendarAssignment> assignments,
                                 Collection<AcademicCalendarDay> calendarDays,
                                 LocalDate startDate,
                                 LocalDate endDate) {
            this.startDate = startDate;
            this.endDate = endDate;
            this.semesterByDate = indexSemesters(semesters, startDate, endDate);
            this.assignmentByGroupAndYear = assignments.stream()
                    .collect(Collectors.toUnmodifiableMap(
                            assignment -> new GroupAcademicYearKey(
                                    assignment.getStudentGroup().getId(),
                                    assignment.getAcademicYear().getId()
                            ),
                            Function.identity()
                    ));
            this.activityByCalendarCourseAndDate = calendarDays.stream()
                    .collect(Collectors.toUnmodifiableMap(
                            day -> new CalendarDayKey(
                                    day.getAcademicCalendar().getId(),
                                    day.getCourseNumber(),
                                    day.getDate()
                            ),
                            AcademicCalendarDay::getActivityType
                    ));
        }

        public Optional<Semester> findSemester(LocalDate date) {
            return Optional.ofNullable(semesterByDate.get(date));
        }

        public boolean hasCalendarAssignment(StudentGroup group, Semester semester) {
            return findAssignment(group, semester).isPresent();
        }

        public Optional<AcademicCalendarActivityType> getActivityType(StudentGroup group,
                                                                      Semester semester,
                                                                      LocalDate date) {
            int courseNumber = courseNumber(group, date);
            if (courseNumber < 1) {
                return Optional.empty();
            }
            return findAssignment(group, semester)
                    .map(StudentGroupCalendarAssignment::getAcademicCalendar)
                    .map(AcademicCalendar::getId)
                    .map(calendarId -> activityByCalendarCourseAndDate.get(
                            new CalendarDayKey(calendarId, courseNumber, date)
                    ));
        }

        public boolean isTheoryDay(StudentGroup group, Semester semester, LocalDate date) {
            return getActivityType(group, semester, date)
                    .map(AcademicCalendarActivityType::getAllowSchedule)
                    .orElse(false);
        }

        public LocalDate startDate() {
            return startDate;
        }

        public LocalDate endDate() {
            return endDate;
        }

        private Optional<StudentGroupCalendarAssignment> findAssignment(StudentGroup group, Semester semester) {
            if (group == null
                    || group.getId() == null
                    || semester == null
                    || semester.getAcademicYear() == null
                    || semester.getAcademicYear().getId() == null) {
                return Optional.empty();
            }
            return Optional.ofNullable(assignmentByGroupAndYear.get(
                    new GroupAcademicYearKey(group.getId(), semester.getAcademicYear().getId())
            ));
        }

        private static Map<LocalDate, Semester> indexSemesters(Collection<Semester> semesters,
                                                                LocalDate startDate,
                                                                LocalDate endDate) {
            if (startDate == null || endDate == null || endDate.isBefore(startDate)) {
                return Map.of();
            }
            Map<LocalDate, Semester> result = new HashMap<>();
            for (Semester semester : semesters) {
                LocalDate effectiveStart = semester.getStartDate().isAfter(startDate)
                        ? semester.getStartDate()
                        : startDate;
                LocalDate effectiveEnd = semester.getEndDate().isBefore(endDate)
                        ? semester.getEndDate()
                        : endDate;
                for (LocalDate date = effectiveStart; !date.isAfter(effectiveEnd); date = date.plusDays(1)) {
                    Semester previous = result.putIfAbsent(date, semester);
                    if (previous != null && !Objects.equals(previous.getId(), semester.getId())) {
                        throw new IllegalStateException("На одну дату назначено несколько семестров");
                    }
                }
            }
            return Map.copyOf(result);
        }
    }

    private static int courseNumber(StudentGroup group, LocalDate date) {
        if (group == null || date == null || group.getYearStartStudy() == null) {
            return 0;
        }
        int academicYearStart = date.getMonthValue() >= 9 ? date.getYear() : date.getYear() - 1;
        return academicYearStart - group.getYearStartStudy() + 1;
    }

    private record GroupAcademicYearKey(Long groupId, Long academicYearId) {
    }

    private record CalendarDayKey(Long calendarId, Integer courseNumber, LocalDate date) {
    }
}
