package com.magistr.app.service;

import com.magistr.app.dto.DepartmentTransferRequest;
import com.magistr.app.model.Department;
import com.magistr.app.model.Role;
import com.magistr.app.model.TeacherDepartmentAssignment;
import com.magistr.app.model.User;
import com.magistr.app.repository.DepartmentRepository;
import com.magistr.app.repository.TeacherDepartmentAssignmentRepository;
import com.magistr.app.repository.UserRepository;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

@Service
public class TeacherDepartmentService {

    private final TeacherDepartmentAssignmentRepository assignmentRepository;
    private final UserRepository userRepository;
    private final DepartmentRepository departmentRepository;
    private final BusinessTimeService businessTime;

    public TeacherDepartmentService(TeacherDepartmentAssignmentRepository assignmentRepository,
                                    UserRepository userRepository,
                                    DepartmentRepository departmentRepository) {
        this(assignmentRepository, userRepository, departmentRepository,
                BusinessTimeService.systemDefault());
    }

    @Autowired
    public TeacherDepartmentService(TeacherDepartmentAssignmentRepository assignmentRepository,
                                    UserRepository userRepository,
                                    DepartmentRepository departmentRepository,
                                    BusinessTimeService businessTime) {
        this.assignmentRepository = assignmentRepository;
        this.userRepository = userRepository;
        this.departmentRepository = departmentRepository;
        this.businessTime = businessTime;
    }

    @Transactional(readOnly = true)
    public List<User> findTeachersForDepartmentAtDate(Long departmentId, LocalDate date) {
        if (departmentId == null || date == null) {
            return List.of();
        }
        Map<Long, User> teachersById = new LinkedHashMap<>();
        assignmentRepository.findDepartmentTeachersAtDate(departmentId, date).stream()
                .map(TeacherDepartmentAssignment::getTeacher)
                .filter(Objects::nonNull)
                .filter(teacher -> teacher.getRole() == Role.TEACHER)
                .filter(teacher -> teacher.isActiveOn(date))
                .forEach(teacher -> teachersById.putIfAbsent(teacher.getId(), teacher));
        return List.copyOf(teachersById.values());
    }

    @Transactional(readOnly = true)
    public boolean hasAssignmentAtDate(Long teacherId, Long departmentId, LocalDate date) {
        return teacherId != null
                && departmentId != null
                && date != null
                && assignmentRepository.existsActiveAssignment(teacherId, departmentId, date);
    }

    @Transactional(readOnly = true)
    public Optional<Long> findPrimaryDepartmentIdAtDate(Long teacherId, LocalDate date) {
        if (teacherId == null || date == null) {
            return Optional.empty();
        }
        return assignmentRepository.findPrimaryAtDate(teacherId, date)
                .map(TeacherDepartmentAssignment::getDepartment)
                .filter(Objects::nonNull)
                .map(Department::getId);
    }

    @Transactional(readOnly = true)
    public Map<Long, List<TeacherDepartmentAssignment>> findAssignmentsForTeachersBetween(
            Collection<Long> teacherIds,
            LocalDate startDate,
            LocalDate endDate
    ) {
        if (teacherIds == null || teacherIds.isEmpty() || startDate == null || endDate == null) {
            return Map.of();
        }
        Set<Long> normalizedIds = teacherIds.stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        if (normalizedIds.isEmpty()) {
            return Map.of();
        }
        return assignmentRepository.findForTeachersBetween(normalizedIds, startDate, endDate).stream()
                .collect(Collectors.groupingBy(
                        assignment -> assignment.getTeacher().getId(),
                        LinkedHashMap::new,
                        Collectors.toList()
                ));
    }

    public Optional<Long> resolveDepartmentIdAtDate(
            Collection<TeacherDepartmentAssignment> assignments,
            LocalDate date
    ) {
        if (assignments == null || date == null) {
            return Optional.empty();
        }
        return assignments.stream()
                .filter(Objects::nonNull)
                .filter(assignment -> isActiveAt(assignment, date))
                .filter(assignment -> assignment.getDepartment() != null)
                .sorted(Comparator
                        .comparing(
                                (TeacherDepartmentAssignment assignment) ->
                                        !Boolean.TRUE.equals(assignment.getPrimaryAssignment())
                        )
                        .thenComparing(
                                TeacherDepartmentAssignment::getValidFrom,
                                Comparator.nullsLast(Comparator.reverseOrder())
                        )
                        .thenComparing(
                                TeacherDepartmentAssignment::getId,
                                Comparator.nullsLast(Comparator.reverseOrder())
                        ))
                .map(assignment -> assignment.getDepartment().getId())
                .findFirst();
    }

    @Transactional
    public TeacherDepartmentAssignment transferTeacher(Long teacherId,
                                                        DepartmentTransferRequest request,
                                                        LocalDate today,
                                                        Long createdBy) {
        if (request == null || request.departmentId() == null) {
            throw new IllegalArgumentException("Кафедра обязательна");
        }
        LocalDate operationDate = today == null ? businessTime.today() : today;
        LocalDate validFrom = request.validFrom() == null ? operationDate : request.validFrom();
        User teacher = userRepository.findByIdForUpdate(teacherId)
                .orElseThrow(() -> new IllegalArgumentException("Преподаватель не найден"));
        if (teacher.getRole() != Role.TEACHER) {
            throw new IllegalArgumentException("Преподаватель не найден");
        }
        if (teacher.isArchivedRecord()) {
            throw new IllegalArgumentException("Архивного преподавателя нельзя перевести");
        }
        Department department = departmentRepository.findById(request.departmentId())
                .filter(candidate -> !candidate.isArchivedRecord())
                .orElseThrow(() -> new IllegalArgumentException("Активная кафедра не найдена"));

        List<TeacherDepartmentAssignment> primaryHistory = new ArrayList<>(
                assignmentRepository.findPrimaryHistoryForUpdate(teacherId)
        );
        TeacherDepartmentAssignment sameDate = primaryHistory.stream()
                .filter(assignment -> validFrom.equals(assignment.getValidFrom()))
                .findFirst()
                .orElse(null);
        if (sameDate != null) {
            sameDate.setDepartment(department);
            sameDate.setComment(normalizeComment(request.comment()));
            sameDate.setCreatedBy(createdBy);
            TeacherDepartmentAssignment saved = saveAssignment(sameDate);
            synchronizeLegacyDepartment(teacher, primaryHistory, operationDate);
            return saved;
        }

        primaryHistory.stream()
                .filter(assignment -> isActiveAt(assignment, validFrom))
                .findFirst()
                .ifPresent(current -> current.setValidTo(validFrom.minusDays(1)));

        LocalDate nextValidFrom = primaryHistory.stream()
                .map(TeacherDepartmentAssignment::getValidFrom)
                .filter(date -> date != null && date.isAfter(validFrom))
                .min(LocalDate::compareTo)
                .orElse(null);

        assignmentRepository.flush();
        TeacherDepartmentAssignment assignment = new TeacherDepartmentAssignment();
        assignment.setTeacher(teacher);
        assignment.setDepartment(department);
        assignment.setValidFrom(validFrom);
        assignment.setValidTo(nextValidFrom == null ? null : nextValidFrom.minusDays(1));
        assignment.setPrimaryAssignment(true);
        assignment.setComment(normalizeComment(request.comment()));
        assignment.setCreatedBy(createdBy);
        TeacherDepartmentAssignment saved = saveAssignment(assignment);

        primaryHistory.add(saved);
        synchronizeLegacyDepartment(teacher, primaryHistory, operationDate);
        return saved;
    }

    private void synchronizeLegacyDepartment(User teacher,
                                             List<TeacherDepartmentAssignment> primaryHistory,
                                             LocalDate date) {
        resolveDepartmentIdAtDate(primaryHistory, date).ifPresent(departmentId -> {
            if (!Objects.equals(teacher.getDepartmentId(), departmentId)) {
                teacher.setDepartmentId(departmentId);
                userRepository.save(teacher);
            }
        });
    }

    private TeacherDepartmentAssignment saveAssignment(TeacherDepartmentAssignment assignment) {
        try {
            return assignmentRepository.saveAndFlush(assignment);
        } catch (DataIntegrityViolationException exception) {
            throw new ScheduleConflictException(
                    "Период основной кафедры пересекается с другим назначением преподавателя"
            );
        }
    }

    private boolean isActiveAt(TeacherDepartmentAssignment assignment, LocalDate date) {
        return assignment.getValidFrom() != null
                && !assignment.getValidFrom().isAfter(date)
                && (assignment.getValidTo() == null || !assignment.getValidTo().isBefore(date));
    }

    private String normalizeComment(String comment) {
        return comment == null || comment.isBlank() ? null : comment.trim();
    }
}
