package com.magistr.app.service;

import com.magistr.app.dto.AcademicCalendarGridDayDto;
import com.magistr.app.model.AcademicCalendar;
import com.magistr.app.model.AcademicCalendarActivityType;
import com.magistr.app.model.AcademicCalendarDay;
import com.magistr.app.model.AcademicYear;
import com.magistr.app.repository.AcademicCalendarActivityTypeRepository;
import com.magistr.app.repository.AcademicCalendarDayRepository;
import com.magistr.app.repository.AcademicCalendarRepository;
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.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;

@Service
public class AcademicCalendarGridService {

    private final AcademicCalendarRepository calendarRepository;
    private final AcademicCalendarDayRepository calendarDayRepository;
    private final AcademicCalendarActivityTypeRepository activityTypeRepository;
    private final ScheduleGeneratorService scheduleGeneratorService;

    public AcademicCalendarGridService(AcademicCalendarRepository calendarRepository,
                                       AcademicCalendarDayRepository calendarDayRepository,
                                       AcademicCalendarActivityTypeRepository activityTypeRepository,
                                       ScheduleGeneratorService scheduleGeneratorService) {
        this.calendarRepository = calendarRepository;
        this.calendarDayRepository = calendarDayRepository;
        this.activityTypeRepository = activityTypeRepository;
        this.scheduleGeneratorService = scheduleGeneratorService;
    }

    @Transactional
    public void replaceGrid(Long id, List<AcademicCalendarGridDayDto> rows) {
        AcademicCalendar calendar = calendarRepository.findByIdWithDetails(id)
                .orElseThrow(() -> new NoSuchElementException("Календарный график не найден"));
        if (rows == null || rows.isEmpty()) {
            throw new IllegalArgumentException("Передайте хотя бы одну ячейку графика");
        }

        List<AcademicCalendarDay> replacement = buildReplacement(calendar, rows);

        calendarDayRepository.deleteByCalendarId(id);
        calendarDayRepository.flush();
        calendarDayRepository.saveAll(replacement);
        calendarDayRepository.flush();
        clearScheduleCacheAfterCommit();
    }

    private List<AcademicCalendarDay> buildReplacement(AcademicCalendar calendar,
                                                       List<AcademicCalendarGridDayDto> rows) {
        List<AcademicCalendarDay> replacement = new ArrayList<>(rows.size());
        Set<GridBusinessKey> uniqueRows = new HashSet<>();

        for (AcademicCalendarGridDayDto row : rows) {
            if (row == null) {
                throw new IllegalArgumentException("Строка календарного графика не может быть пустой");
            }
            validateRequiredFields(row);
            validateCourse(calendar, row.courseNumber());
            validateDate(calendar.getAcademicYear(), row.date());
            validateWeek(calendar.getAcademicYear(), row.date(), row.weekNumber());
            validateDayOfWeek(row.date(), row.dayOfWeek());

            GridBusinessKey businessKey = new GridBusinessKey(row.courseNumber(), row.date());
            if (!uniqueRows.add(businessKey)) {
                throw new IllegalArgumentException("Для курса и даты можно указать только одну активность");
            }

            AcademicCalendarDay day = new AcademicCalendarDay();
            day.setAcademicCalendar(calendar);
            day.setCourseNumber(row.courseNumber());
            day.setDate(row.date());
            day.setWeekNumber(row.weekNumber());
            day.setDayOfWeek(row.dayOfWeek());
            day.setActivityType(resolveActivityType(row));
            replacement.add(day);
        }
        return replacement;
    }

    private void validateRequiredFields(AcademicCalendarGridDayDto row) {
        if (row.courseNumber() == null
                || row.date() == null
                || row.weekNumber() == null
                || row.weekNumber() <= 0
                || row.dayOfWeek() == null
                || row.dayOfWeek() < 1
                || row.dayOfWeek() > 7) {
            throw new IllegalArgumentException("Курс, дата, неделя и день недели обязательны");
        }
    }

    private void validateCourse(AcademicCalendar calendar, Integer courseNumber) {
        if (courseNumber < 1 || courseNumber > calendar.getCourseCount()) {
            throw new IllegalArgumentException("Номер курса выходит за пределы графика");
        }
    }

    private void validateDate(AcademicYear academicYear, LocalDate date) {
        if (date.isBefore(academicYear.getStartDate()) || date.isAfter(academicYear.getEndDate())) {
            throw new IllegalArgumentException("Дата выходит за пределы учебного года");
        }
    }

    private void validateWeek(AcademicYear academicYear, LocalDate date, Integer weekNumber) {
        long daysFromYearStart = ChronoUnit.DAYS.between(academicYear.getStartDate(), date);
        int daysBeforeYearStartInFirstWeek = academicYear.getStartDate().getDayOfWeek().getValue() - 1;
        int expectedWeek = Math.toIntExact((daysBeforeYearStartInFirstWeek + daysFromYearStart) / 7 + 1);
        if (weekNumber != expectedWeek) {
            throw new IllegalArgumentException("Номер недели не соответствует дате учебного года");
        }
    }

    private void validateDayOfWeek(LocalDate date, Integer dayOfWeek) {
        if (dayOfWeek != date.getDayOfWeek().getValue()) {
            throw new IllegalArgumentException("День недели не соответствует дате");
        }
    }

    private AcademicCalendarActivityType resolveActivityType(AcademicCalendarGridDayDto row) {
        if (row.activityTypeId() != null) {
            return activityTypeRepository.findById(row.activityTypeId())
                    .orElseThrow(() -> new IllegalArgumentException("Код активности не найден"));
        }
        if (row.activityCode() != null && !row.activityCode().isBlank()) {
            return activityTypeRepository.findByCode(row.activityCode().trim())
                    .orElseThrow(() -> new IllegalArgumentException("Код активности не найден"));
        }
        throw new IllegalArgumentException("Код активности обязателен");
    }

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

    private record GridBusinessKey(Integer courseNumber, LocalDate date) {
    }
}
