package com.magistr.app.service;

import com.magistr.app.dto.AcademicCalendarDto;
import com.magistr.app.dto.CreateGroupRequest;
import com.magistr.app.dto.GroupCalendarAssignmentDto;
import com.magistr.app.model.AcademicCalendar;
import com.magistr.app.model.AcademicYear;
import com.magistr.app.model.EducationForm;
import com.magistr.app.model.Speciality;
import com.magistr.app.model.SpecialtyProfile;
import com.magistr.app.model.StudentGroup;
import com.magistr.app.model.StudentGroupCalendarAssignment;
import com.magistr.app.repository.AcademicCalendarDayRepository;
import com.magistr.app.repository.AcademicCalendarRepository;
import com.magistr.app.repository.AcademicCalendarSubjectRepository;
import com.magistr.app.repository.AcademicYearRepository;
import com.magistr.app.repository.EducationFormRepository;
import com.magistr.app.repository.GroupRepository;
import com.magistr.app.repository.SpecialtiesRepository;
import com.magistr.app.repository.SpecialtyProfileRepository;
import com.magistr.app.repository.StudentGroupCalendarAssignmentRepository;
import com.magistr.app.repository.SubgroupRepository;
import com.magistr.app.utils.AcademicCalendarTitleBuilder;
import com.magistr.app.utils.CourseAndSemesterCalculator;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.List;
import java.util.NoSuchElementException;

@Service
public class AcademicStructureService {

    private final AcademicCalendarRepository calendarRepository;
    private final AcademicCalendarDayRepository calendarDayRepository;
    private final AcademicCalendarSubjectRepository calendarSubjectRepository;
    private final AcademicYearRepository academicYearRepository;
    private final SpecialtiesRepository specialtiesRepository;
    private final SpecialtyProfileRepository profileRepository;
    private final EducationFormRepository educationFormRepository;
    private final GroupRepository groupRepository;
    private final StudentGroupCalendarAssignmentRepository assignmentRepository;
    private final SubgroupRepository subgroupRepository;
    private final ScheduleGeneratorService scheduleGeneratorService;

    public AcademicStructureService(AcademicCalendarRepository calendarRepository,
                                    AcademicCalendarDayRepository calendarDayRepository,
                                    AcademicCalendarSubjectRepository calendarSubjectRepository,
                                    AcademicYearRepository academicYearRepository,
                                    SpecialtiesRepository specialtiesRepository,
                                    SpecialtyProfileRepository profileRepository,
                                    EducationFormRepository educationFormRepository,
                                    GroupRepository groupRepository,
                                    StudentGroupCalendarAssignmentRepository assignmentRepository,
                                    SubgroupRepository subgroupRepository,
                                    ScheduleGeneratorService scheduleGeneratorService) {
        this.calendarRepository = calendarRepository;
        this.calendarDayRepository = calendarDayRepository;
        this.calendarSubjectRepository = calendarSubjectRepository;
        this.academicYearRepository = academicYearRepository;
        this.specialtiesRepository = specialtiesRepository;
        this.profileRepository = profileRepository;
        this.educationFormRepository = educationFormRepository;
        this.groupRepository = groupRepository;
        this.assignmentRepository = assignmentRepository;
        this.subgroupRepository = subgroupRepository;
        this.scheduleGeneratorService = scheduleGeneratorService;
    }

    @Transactional
    public StudentGroup createGroup(CreateGroupRequest request) {
        validateGroupRequest(request);
        GroupDimensions candidate = resolveGroupDimensions(request);

        StudentGroup group = new StudentGroup();
        applyGroupDimensions(group, request, candidate);
        StudentGroup saved = saveGroup(group);
        clearScheduleCacheAfterCommit();
        return saved;
    }

    @Transactional
    public AcademicCalendar updateCalendar(Long id, AcademicCalendarDto request) {
        validateCalendarRequest(request);
        AcademicCalendar calendar = calendarRepository.findByIdWithDetailsForUpdate(id)
                .orElseThrow(() -> new NoSuchElementException("Календарный график не найден"));
        CalendarDimensions candidate = resolveCalendarDimensions(request);

        List<StudentGroupCalendarAssignment> assignments =
                assignmentRepository.findByCalendarIdWithDetails(id);
        boolean incompatibleAssignment = assignments.stream()
                .anyMatch(assignment -> !isCompatible(
                        assignment.getStudentGroup(),
                        assignment.getAcademicYear(),
                        candidate.academicYear(),
                        candidate.speciality(),
                        candidate.profile(),
                        candidate.studyForm(),
                        request.courseCount()
                ));
        if (incompatibleAssignment) {
            throw new ScheduleConflictException(
                    "Нельзя изменить график: сохранённые назначения групп станут несовместимыми"
            );
        }
        if (calendarDayRepository.existsByAcademicCalendarIdAndCourseNumberGreaterThan(
                id, request.courseCount())) {
            throw new ScheduleConflictException(
                    "Нельзя уменьшить количество курсов: в сетке есть данные старших курсов"
            );
        }
        if (calendarSubjectRepository.existsByAcademicCalendarIdAndSemesterNumberGreaterThan(
                id, request.courseCount() * 2)) {
            throw new ScheduleConflictException(
                    "Нельзя уменьшить количество курсов: есть дисциплины старших семестров"
            );
        }
        if (calendarDayRepository.existsOutsideDateRange(
                id,
                candidate.academicYear().getStartDate(),
                candidate.academicYear().getEndDate())) {
            throw new ScheduleConflictException(
                    "Нельзя изменить учебный год графика: сохранённая сетка содержит даты за его границами"
            );
        }

        calendar.setTitle(AcademicCalendarTitleBuilder.build(
                candidate.academicYear(),
                candidate.speciality(),
                candidate.profile(),
                candidate.studyForm()
        ));
        applyCalendarDimensions(calendar, candidate, request.courseCount());
        AcademicCalendar saved = saveCalendar(calendar);
        clearScheduleCacheAfterCommit();
        return saved;
    }

    @Transactional
    public StudentGroup updateGroup(Long id, CreateGroupRequest request) {
        validateGroupRequest(request);
        StudentGroup group = groupRepository.findByIdForUpdate(id)
                .orElseThrow(() -> new NoSuchElementException("Учебная группа не найдена"));
        long activeSubgroupCapacity = subgroupRepository.sumActiveStudentCapacityByGroupId(id);
        if (request.getGroupSize() < activeSubgroupCapacity) {
            throw new IllegalArgumentException(
                    "Численность группы не может быть меньше суммы численностей активных подгрупп ("
                            + activeSubgroupCapacity + " чел.)"
            );
        }
        GroupDimensions candidate = resolveGroupDimensions(request);

        boolean incompatibleAssignment = assignmentRepository.findByGroupIdWithDetails(id).stream()
                .anyMatch(assignment -> !isCompatible(
                        request.getYearStartStudy(),
                        candidate.speciality(),
                        candidate.profile(),
                        candidate.studyForm(),
                        assignment.getAcademicYear(),
                        assignment.getAcademicCalendar()
                ));
        if (incompatibleAssignment) {
            throw new ScheduleConflictException(
                    "Нельзя изменить группу: сохранённые назначения календарных графиков станут несовместимыми"
            );
        }

        applyGroupDimensions(group, request, candidate);
        StudentGroup saved = saveGroup(group);
        clearScheduleCacheAfterCommit();
        return saved;
    }

    @Transactional
    public StudentGroupCalendarAssignment saveAssignment(Long groupId,
                                                         GroupCalendarAssignmentDto request) {
        if (request == null || request.academicYearId() == null || request.calendarId() == null) {
            throw new IllegalArgumentException("Учебный год и календарный график обязательны");
        }
        StudentGroup group = groupRepository.findByIdForUpdate(groupId)
                .orElseThrow(() -> new NoSuchElementException("Учебная группа не найдена"));
        AcademicCalendar calendar = calendarRepository.findByIdWithDetailsForUpdate(request.calendarId())
                .orElseThrow(() -> new IllegalArgumentException("Календарный график не найден"));
        AcademicYear academicYear = academicYearRepository.findByIdForUpdate(request.academicYearId())
                .orElseThrow(() -> new IllegalArgumentException("Учебный год не найден"));

        if (!isCompatible(group, academicYear, calendar)) {
            throw new IllegalArgumentException(
                    "Календарный график не соответствует учебному году, параметрам или курсу группы"
            );
        }

        StudentGroupCalendarAssignment assignment = assignmentRepository
                .findByGroupIdAndAcademicYearIdForUpdate(groupId, academicYear.getId())
                .orElseGet(StudentGroupCalendarAssignment::new);
        assignment.setStudentGroup(group);
        assignment.setAcademicYear(academicYear);
        assignment.setAcademicCalendar(calendar);
        try {
            StudentGroupCalendarAssignment saved = assignmentRepository.saveAndFlush(assignment);
            clearScheduleCacheAfterCommit();
            return saved;
        } catch (DataIntegrityViolationException exception) {
            throw new ScheduleConflictException(
                    "Назначение календарного графика было изменено конкурентно"
            );
        }
    }

    private void validateCalendarRequest(AcademicCalendarDto request) {
        if (request == null) {
            throw new IllegalArgumentException("Передайте данные календарного графика");
        }
        if (request.courseCount() == null || request.courseCount() <= 0) {
            throw new IllegalArgumentException("Количество курсов должно быть больше нуля");
        }
        if (request.courseCount() > 8) {
            throw new IllegalArgumentException("Количество курсов не может быть больше 8");
        }
        if (request.academicYearId() == null
                || request.specialtyId() == null
                || request.specialtyProfileId() == null
                || request.studyFormId() == null) {
            throw new IllegalArgumentException(
                    "Учебный год, специальность, профиль и форма обучения обязательны"
            );
        }
    }

    private void validateGroupRequest(CreateGroupRequest request) {
        if (request == null) {
            throw new IllegalArgumentException("Передайте данные учебной группы");
        }
        if (request.getName() == null || request.getName().isBlank()) {
            throw new IllegalArgumentException("Название группы обязательно");
        }
        if (request.getGroupSize() == null || request.getGroupSize() <= 0) {
            throw new IllegalArgumentException("Численность группы должна быть больше нуля");
        }
        if (request.getEducationFormId() == null || request.getEducationFormId() <= 0) {
            throw new IllegalArgumentException("Форма обучения обязательна");
        }
        if (request.getDepartmentId() == null || request.getDepartmentId() <= 0) {
            throw new IllegalArgumentException("ID кафедры обязателен");
        }
        if (request.getYearStartStudy() == null || request.getYearStartStudy() <= 0) {
            throw new IllegalArgumentException("Год начала обучения должен быть больше нуля");
        }
        if (request.getEffectiveSpecialtyId() == null || request.getEffectiveSpecialtyId() <= 0) {
            throw new IllegalArgumentException("Код специальности обязателен");
        }
        if (request.getSpecialtyProfileId() == null || request.getSpecialtyProfileId() <= 0) {
            throw new IllegalArgumentException("Профиль обучения обязателен");
        }
    }

    private void applyGroupDimensions(StudentGroup group,
                                      CreateGroupRequest request,
                                      GroupDimensions candidate) {
        group.setName(request.getName().trim());
        group.setGroupSize(request.getGroupSize());
        group.setEducationForm(candidate.studyForm());
        group.setDepartmentId(request.getDepartmentId());
        group.setYearStartStudy(request.getYearStartStudy());
        group.setSpeciality(candidate.speciality());
        group.setSpecialtyProfile(candidate.profile());
    }

    private CalendarDimensions resolveCalendarDimensions(AcademicCalendarDto request) {
        AcademicYear academicYear = academicYearRepository.findById(request.academicYearId())
                .orElseThrow(() -> new IllegalArgumentException("Учебный год не найден"));
        Speciality speciality = specialtiesRepository.findById(request.specialtyId())
                .orElseThrow(() -> new IllegalArgumentException("Специальность не найдена"));
        SpecialtyProfile profile = resolveProfile(request.specialtyProfileId(), speciality);
        EducationForm studyForm = educationFormRepository.findById(request.studyFormId())
                .orElseThrow(() -> new IllegalArgumentException("Форма обучения не найдена"));
        return new CalendarDimensions(academicYear, speciality, profile, studyForm);
    }

    private GroupDimensions resolveGroupDimensions(CreateGroupRequest request) {
        EducationForm studyForm = educationFormRepository.findById(request.getEducationFormId())
                .orElseThrow(() -> new IllegalArgumentException("Форма обучения не найдена"));
        Speciality speciality = specialtiesRepository.findById(request.getEffectiveSpecialtyId())
                .orElseThrow(() -> new IllegalArgumentException("Специальность не найдена"));
        SpecialtyProfile profile = resolveProfile(request.getSpecialtyProfileId(), speciality);
        return new GroupDimensions(speciality, profile, studyForm);
    }

    private SpecialtyProfile resolveProfile(Long profileId, Speciality speciality) {
        SpecialtyProfile profile = profileRepository.findById(profileId)
                .orElseThrow(() -> new IllegalArgumentException(
                        "Профиль обучения не найден для выбранной специальности"
                ));
        if (!profile.getSpeciality().getId().equals(speciality.getId())) {
            throw new IllegalArgumentException(
                    "Профиль обучения не найден для выбранной специальности"
            );
        }
        return profile;
    }

    private boolean isCompatible(StudentGroup group,
                                 AcademicYear assignmentYear,
                                 AcademicCalendar calendar) {
        return isCompatible(
                group,
                assignmentYear,
                calendar.getAcademicYear(),
                calendar.getSpeciality(),
                calendar.getSpecialtyProfile(),
                calendar.getStudyForm(),
                calendar.getCourseCount()
        );
    }

    private boolean isCompatible(StudentGroup group,
                                 AcademicYear assignmentYear,
                                 AcademicYear calendarYear,
                                 Speciality calendarSpeciality,
                                 SpecialtyProfile calendarProfile,
                                 EducationForm calendarStudyForm,
                                 Integer courseCount) {
        return isCompatible(
                group.getYearStartStudy(),
                group.getSpeciality(),
                group.getSpecialtyProfile(),
                group.getEducationForm(),
                assignmentYear,
                calendarYear,
                calendarSpeciality,
                calendarProfile,
                calendarStudyForm,
                courseCount
        );
    }

    private boolean isCompatible(Integer yearStartStudy,
                                 Speciality groupSpeciality,
                                 SpecialtyProfile groupProfile,
                                 EducationForm groupStudyForm,
                                 AcademicYear assignmentYear,
                                 AcademicCalendar calendar) {
        return isCompatible(
                yearStartStudy,
                groupSpeciality,
                groupProfile,
                groupStudyForm,
                assignmentYear,
                calendar.getAcademicYear(),
                calendar.getSpeciality(),
                calendar.getSpecialtyProfile(),
                calendar.getStudyForm(),
                calendar.getCourseCount()
        );
    }

    private boolean isCompatible(Integer yearStartStudy,
                                 Speciality groupSpeciality,
                                 SpecialtyProfile groupProfile,
                                 EducationForm groupStudyForm,
                                 AcademicYear assignmentYear,
                                 AcademicYear calendarYear,
                                 Speciality calendarSpeciality,
                                 SpecialtyProfile calendarProfile,
                                 EducationForm calendarStudyForm,
                                 Integer courseCount) {
        if (!sameId(assignmentYear, calendarYear)
                || !sameId(groupSpeciality, calendarSpeciality)
                || !sameId(groupProfile, calendarProfile)
                || !sameId(groupStudyForm, calendarStudyForm)) {
            return false;
        }
        int course = CourseAndSemesterCalculator.getActualCourse(
                yearStartStudy,
                assignmentYear.getStartDate()
        );
        return course >= 1 && courseCount != null && course <= courseCount;
    }

    private boolean sameId(AcademicYear first, AcademicYear second) {
        return first != null && second != null && first.getId().equals(second.getId());
    }

    private boolean sameId(Speciality first, Speciality second) {
        return first != null && second != null && first.getId().equals(second.getId());
    }

    private boolean sameId(SpecialtyProfile first, SpecialtyProfile second) {
        return first != null && second != null && first.getId().equals(second.getId());
    }

    private boolean sameId(EducationForm first, EducationForm second) {
        return first != null && second != null && first.getId().equals(second.getId());
    }

    private void applyCalendarDimensions(AcademicCalendar calendar,
                                         CalendarDimensions dimensions,
                                         Integer courseCount) {
        calendar.setAcademicYear(dimensions.academicYear());
        calendar.setSpeciality(dimensions.speciality());
        calendar.setSpecialtyProfile(dimensions.profile());
        calendar.setStudyForm(dimensions.studyForm());
        calendar.setCourseCount(courseCount);
    }

    private AcademicCalendar saveCalendar(AcademicCalendar calendar) {
        try {
            return calendarRepository.saveAndFlush(calendar);
        } catch (DataIntegrityViolationException exception) {
            throw new ScheduleConflictException(
                    "Изменение календарного графика конфликтует с сохранёнными данными"
            );
        }
    }

    private StudentGroup saveGroup(StudentGroup group) {
        try {
            return groupRepository.saveAndFlush(group);
        } catch (DataIntegrityViolationException exception) {
            throw new ScheduleConflictException(
                    "Изменение учебной группы конфликтует с сохранёнными данными"
            );
        }
    }

    private void clearScheduleCacheAfterCommit() {
        if (!TransactionSynchronizationManager.isSynchronizationActive()
                || !TransactionSynchronizationManager.isActualTransactionActive()) {
            scheduleGeneratorService.clearCache();
            return;
        }
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
            @Override
            public void afterCommit() {
                scheduleGeneratorService.clearCache();
            }
        });
    }

    private record CalendarDimensions(AcademicYear academicYear,
                                      Speciality speciality,
                                      SpecialtyProfile profile,
                                      EducationForm studyForm) {
    }

    private record GroupDimensions(Speciality speciality,
                                   SpecialtyProfile profile,
                                   EducationForm studyForm) {
    }
}
