package com.magistr.app.service;

import com.magistr.app.dto.TimeSlotDto;
import com.magistr.app.model.TimeSlot;
import com.magistr.app.model.TimeSlotScope;
import com.magistr.app.repository.ScheduleRuleSlotRepository;
import com.magistr.app.repository.TimeSlotRepository;
import com.magistr.app.repository.TimeSlotScopeRepository;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.Duration;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.stream.Stream;

@Service
public class TimeSlotService {

    private static final String APPLY_MODE_DEFAULT = "DEFAULT";
    private static final String SLOT_CONFLICT_MESSAGE =
            "Номер пары или интервал времени уже занят в выбранной сетке";

    private final TimeSlotRepository timeSlotRepository;
    private final TimeSlotScopeRepository timeSlotScopeRepository;
    private final ScheduleRuleSlotRepository scheduleRuleSlotRepository;

    public TimeSlotService(TimeSlotRepository timeSlotRepository,
                           TimeSlotScopeRepository timeSlotScopeRepository,
                           ScheduleRuleSlotRepository scheduleRuleSlotRepository) {
        this.timeSlotRepository = timeSlotRepository;
        this.timeSlotScopeRepository = timeSlotScopeRepository;
        this.scheduleRuleSlotRepository = scheduleRuleSlotRepository;
    }

    @Transactional
    public TimeSlot create(TimeSlotDto request) {
        validate(request);
        TimeSlotScope targetScope = lockScope(request.scopeId());
        ensureUniqueAndNonOverlapping(request, null);

        TimeSlot timeSlot = new TimeSlot();
        apply(timeSlot, targetScope, request);
        return save(timeSlot);
    }

    @Transactional
    public TimeSlot update(Long id, TimeSlotDto request) {
        validate(request);
        TimeSlot timeSlot = timeSlotRepository.findByIdForUpdate(id)
                .orElseThrow(() -> new NoSuchElementException("Временной слот не найден"));

        Long currentScopeId = timeSlot.getTimeSlotScope().getId();
        List<Long> scopeIds = Stream.of(currentScopeId, request.scopeId())
                .filter(Objects::nonNull)
                .distinct()
                .sorted()
                .toList();
        TimeSlotScope targetScope = null;
        for (Long scopeId : scopeIds) {
            TimeSlotScope lockedScope = lockScope(scopeId);
            if (scopeId.equals(request.scopeId())) {
                targetScope = lockedScope;
            }
        }
        if (targetScope == null) {
            throw new NoSuchElementException("Сетка времени не найдена");
        }

        if (scheduleRuleSlotRepository.existsByTimeSlotId(id)
                && !APPLY_MODE_DEFAULT.equals(targetScope.getApplyMode())) {
            throw new IllegalArgumentException(
                    "Слот базовой сетки, используемый в правилах расписания, нельзя перенести в другую сетку"
            );
        }

        ensureUniqueAndNonOverlapping(request, id);
        apply(timeSlot, targetScope, request);
        return save(timeSlot);
    }

    @Transactional
    public void delete(Long id) {
        TimeSlot timeSlot = timeSlotRepository.findByIdForUpdate(id)
                .orElseThrow(() -> new NoSuchElementException("Временной слот не найден"));
        if (scheduleRuleSlotRepository.existsByTimeSlotId(id)) {
            throw new IllegalArgumentException(
                    "Нельзя удалить слот, который используется в правилах расписания"
            );
        }
        try {
            timeSlotRepository.delete(timeSlot);
            timeSlotRepository.flush();
        } catch (DataIntegrityViolationException exception) {
            throw new IllegalArgumentException(
                    "Нельзя удалить слот, который используется в расписании",
                    exception
            );
        }
    }

    private void validate(TimeSlotDto request) {
        if (request == null) {
            throw new IllegalArgumentException("Данные временного слота обязательны");
        }
        if (request.orderNumber() == null || request.orderNumber() <= 0) {
            throw new IllegalArgumentException("Номер пары должен быть больше нуля");
        }
        if (request.scopeId() == null) {
            throw new IllegalArgumentException("Выберите сетку времени");
        }
        if (request.startTime() == null || request.endTime() == null) {
            throw new IllegalArgumentException("Время начала и окончания обязательно");
        }
        if (!request.startTime().isBefore(request.endTime())) {
            throw new IllegalArgumentException("Время начала должно быть раньше времени окончания");
        }
        if (Duration.between(request.startTime(), request.endTime()).toMinutes() < 1) {
            throw new IllegalArgumentException("Длительность временного слота должна быть не меньше одной минуты");
        }
    }

    private TimeSlotScope lockScope(Long scopeId) {
        return timeSlotScopeRepository.findByIdForUpdate(scopeId)
                .orElseThrow(() -> new NoSuchElementException("Сетка времени не найдена"));
    }

    private void ensureUniqueAndNonOverlapping(TimeSlotDto request, Long currentId) {
        boolean duplicateOrder = currentId == null
                ? timeSlotRepository.existsByTimeSlotScopeIdAndOrderNumber(
                        request.scopeId(), request.orderNumber())
                : timeSlotRepository.existsByTimeSlotScopeIdAndOrderNumberAndIdNot(
                        request.scopeId(), request.orderNumber(), currentId);
        boolean overlapping = currentId == null
                ? timeSlotRepository.existsOverlapping(
                        request.scopeId(), request.startTime(), request.endTime())
                : timeSlotRepository.existsOverlappingExcluding(
                        request.scopeId(), currentId, request.startTime(), request.endTime());
        if (duplicateOrder || overlapping) {
            throw new ScheduleConflictException(SLOT_CONFLICT_MESSAGE);
        }
    }

    private void apply(TimeSlot timeSlot, TimeSlotScope scope, TimeSlotDto request) {
        timeSlot.setOrderNumber(request.orderNumber());
        timeSlot.setTimeSlotScope(scope);
        timeSlot.setStartTime(request.startTime());
        timeSlot.setEndTime(request.endTime());
        timeSlot.setDurationMinutes((int) Duration.between(request.startTime(), request.endTime()).toMinutes());
    }

    private TimeSlot save(TimeSlot timeSlot) {
        try {
            return timeSlotRepository.saveAndFlush(timeSlot);
        } catch (DataIntegrityViolationException exception) {
            throw new ScheduleConflictException(SLOT_CONFLICT_MESSAGE);
        }
    }
}
