package com.magistr.app.service;

import com.magistr.app.dto.AcademicYearDto;
import com.magistr.app.dto.SemesterDto;
import com.magistr.app.model.AcademicYear;
import com.magistr.app.model.Semester;
import com.magistr.app.repository.AcademicYearRepository;
import com.magistr.app.repository.SemesterRepository;
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.time.LocalDate;
import java.util.NoSuchElementException;

@Service
public class AcademicPeriodService {

    private static final String YEAR_CONFLICT_MESSAGE =
            "Название или период учебного года конфликтует с существующим учебным годом";
    private static final String SEMESTER_CONFLICT_MESSAGE =
            "Тип или период семестра конфликтует с существующим семестром учебного года";

    private final AcademicYearRepository academicYearRepository;
    private final SemesterRepository semesterRepository;
    private final ScheduleGeneratorService scheduleGeneratorService;

    public AcademicPeriodService(AcademicYearRepository academicYearRepository,
                                 SemesterRepository semesterRepository,
                                 ScheduleGeneratorService scheduleGeneratorService) {
        this.academicYearRepository = academicYearRepository;
        this.semesterRepository = semesterRepository;
        this.scheduleGeneratorService = scheduleGeneratorService;
    }

    @Transactional
    public AcademicYear createYear(AcademicYearDto request) {
        validateYear(request);
        String title = request.title().trim();
        if (academicYearRepository.existsByTitle(title)
                || academicYearRepository.existsOverlapping(request.startDate(), request.endDate())) {
            throw new ScheduleConflictException(YEAR_CONFLICT_MESSAGE);
        }

        AcademicYear year = new AcademicYear();
        applyYear(year, title, request.startDate(), request.endDate());
        AcademicYear saved = saveYear(year);
        clearScheduleCacheAfterCommit();
        return saved;
    }

    @Transactional
    public AcademicYear updateYear(Long id, AcademicYearDto request) {
        validateYear(request);
        AcademicYear year = lockYear(id);
        String title = request.title().trim();

        if (academicYearRepository.existsByTitleAndIdNot(title, id)
                || academicYearRepository.existsOverlappingExcluding(
                        id, request.startDate(), request.endDate())) {
            throw new ScheduleConflictException(YEAR_CONFLICT_MESSAGE);
        }
        boolean semesterOutsideNewRange = semesterRepository
                .findByAcademicYearIdOrderByStartDateAsc(id)
                .stream()
                .anyMatch(semester -> !contains(
                        request.startDate(),
                        request.endDate(),
                        semester.getStartDate(),
                        semester.getEndDate()
                ));
        if (semesterOutsideNewRange) {
            throw new IllegalArgumentException(
                    "Новые границы учебного года не включают все его семестры"
            );
        }

        applyYear(year, title, request.startDate(), request.endDate());
        AcademicYear saved = saveYear(year);
        clearScheduleCacheAfterCommit();
        return saved;
    }

    @Transactional
    public void deleteYear(Long id) {
        AcademicYear year = lockYear(id);
        academicYearRepository.delete(year);
        academicYearRepository.flush();
        clearScheduleCacheAfterCommit();
    }

    @Transactional
    public Semester createSemester(Long academicYearId, SemesterDto request) {
        validateSemester(request);
        AcademicYear year = lockYear(academicYearId);
        validateSemesterWithinYear(year, request.startDate(), request.endDate());
        ensureSemesterAvailable(year.getId(), request, null);

        Semester semester = new Semester();
        semester.setAcademicYear(year);
        applySemester(semester, request);
        Semester saved = saveSemester(semester);
        clearScheduleCacheAfterCommit();
        return saved;
    }

    @Transactional
    public Semester updateSemester(Long id, SemesterDto request) {
        validateSemester(request);
        Semester snapshot = semesterRepository.findById(id)
                .orElseThrow(() -> new NoSuchElementException("Семестр не найден"));
        AcademicYear year = lockYear(snapshot.getAcademicYear().getId());
        Semester semester = semesterRepository.findByIdForUpdate(id)
                .orElseThrow(() -> new NoSuchElementException("Семестр не найден"));
        if (!semester.getAcademicYear().getId().equals(year.getId())) {
            throw new ScheduleConflictException("Учебный год семестра был изменён конкурентно");
        }

        validateSemesterWithinYear(year, request.startDate(), request.endDate());
        ensureSemesterAvailable(year.getId(), request, id);
        applySemester(semester, request);
        Semester saved = saveSemester(semester);
        clearScheduleCacheAfterCommit();
        return saved;
    }

    private void validateYear(AcademicYearDto request) {
        if (request == null) {
            throw new IllegalArgumentException("Данные учебного года обязательны");
        }
        if (request.title() == null || request.title().isBlank()) {
            throw new IllegalArgumentException("Название учебного года обязательно");
        }
        validateDateRange(
                request.startDate(),
                request.endDate(),
                "Даты начала и окончания учебного года обязательны",
                "Дата окончания учебного года не может быть раньше даты начала"
        );
    }

    private void validateSemester(SemesterDto request) {
        if (request == null) {
            throw new IllegalArgumentException("Данные семестра обязательны");
        }
        if (request.semesterType() == null) {
            throw new IllegalArgumentException("Тип семестра обязателен");
        }
        validateDateRange(
                request.startDate(),
                request.endDate(),
                "Даты начала и окончания семестра обязательны",
                "Дата окончания семестра не может быть раньше даты начала"
        );
    }

    private void validateDateRange(LocalDate startDate,
                                   LocalDate endDate,
                                   String requiredMessage,
                                   String orderMessage) {
        if (startDate == null || endDate == null) {
            throw new IllegalArgumentException(requiredMessage);
        }
        if (endDate.isBefore(startDate)) {
            throw new IllegalArgumentException(orderMessage);
        }
    }

    private void validateSemesterWithinYear(AcademicYear year,
                                            LocalDate startDate,
                                            LocalDate endDate) {
        if (!contains(year.getStartDate(), year.getEndDate(), startDate, endDate)) {
            throw new IllegalArgumentException("Семестр должен полностью находиться в границах учебного года");
        }
    }

    private boolean contains(LocalDate outerStart,
                             LocalDate outerEnd,
                             LocalDate innerStart,
                             LocalDate innerEnd) {
        return !innerStart.isBefore(outerStart) && !innerEnd.isAfter(outerEnd);
    }

    private void ensureSemesterAvailable(Long academicYearId, SemesterDto request, Long currentId) {
        boolean duplicateType = currentId == null
                ? semesterRepository.existsByAcademicYearIdAndSemesterType(
                        academicYearId, request.semesterType())
                : semesterRepository.existsByAcademicYearIdAndSemesterTypeAndIdNot(
                        academicYearId, request.semesterType(), currentId);
        boolean overlapping = currentId == null
                ? semesterRepository.existsOverlapping(
                        academicYearId, request.startDate(), request.endDate())
                : semesterRepository.existsOverlappingExcluding(
                        academicYearId, currentId, request.startDate(), request.endDate());
        if (duplicateType || overlapping) {
            throw new ScheduleConflictException(SEMESTER_CONFLICT_MESSAGE);
        }
    }

    private AcademicYear lockYear(Long id) {
        return academicYearRepository.findByIdForUpdate(id)
                .orElseThrow(() -> new NoSuchElementException("Учебный год не найден"));
    }

    private void applyYear(AcademicYear year,
                           String title,
                           LocalDate startDate,
                           LocalDate endDate) {
        year.setTitle(title);
        year.setStartDate(startDate);
        year.setEndDate(endDate);
    }

    private void applySemester(Semester semester, SemesterDto request) {
        semester.setSemesterType(request.semesterType());
        semester.setStartDate(request.startDate());
        semester.setEndDate(request.endDate());
    }

    private AcademicYear saveYear(AcademicYear year) {
        try {
            return academicYearRepository.saveAndFlush(year);
        } catch (DataIntegrityViolationException exception) {
            throw new ScheduleConflictException(YEAR_CONFLICT_MESSAGE);
        }
    }

    private Semester saveSemester(Semester semester) {
        try {
            return semesterRepository.saveAndFlush(semester);
        } catch (DataIntegrityViolationException exception) {
            throw new ScheduleConflictException(SEMESTER_CONFLICT_MESSAGE);
        }
    }

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