package com.magistr.app.controller;

import com.magistr.app.config.auth.AuthContext;
import com.magistr.app.config.auth.RequireRoles;
import com.magistr.app.dto.ClassroomResponse;
import com.magistr.app.dto.RenderedLessonDto;
import com.magistr.app.dto.WorkloadSummaryDto;
import com.magistr.app.model.*;
import com.magistr.app.repository.ClassroomRepository;
import com.magistr.app.repository.DepartmentRepository;
import com.magistr.app.service.ScheduleQueryService;
import com.magistr.app.service.TeacherDepartmentService;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

import java.time.LocalDate;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/workload")
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
public class WorkloadController {

    private static final int ACADEMIC_HOURS_PER_LESSON = 2;

    private final ScheduleQueryService scheduleQueryService;
    private final ClassroomRepository classroomRepository;
    private final DepartmentRepository departmentRepository;
    private final TeacherDepartmentService teacherDepartmentService;

    public WorkloadController(ScheduleQueryService scheduleQueryService,
                              ClassroomRepository classroomRepository,
                              DepartmentRepository departmentRepository,
                              TeacherDepartmentService teacherDepartmentService) {
        this.scheduleQueryService = scheduleQueryService;
        this.classroomRepository = classroomRepository;
        this.departmentRepository = departmentRepository;
        this.teacherDepartmentService = teacherDepartmentService;
    }

    @GetMapping("/teachers")
    public List<WorkloadSummaryDto> teacherWorkload(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
            @RequestParam(required = false) Long departmentId
    ) {
        Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
        List<RenderedLessonDto> lessons = scheduleQueryService.searchForAggregation(
                effectiveDepartmentId, null, startDate, endDate
        );
        return summarizeTeacherWorkload(lessons, startDate, endDate);
    }

    @GetMapping("/classrooms")
    public List<WorkloadSummaryDto> classroomWorkload(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
            @RequestParam(required = false) Long departmentId
    ) {
        Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
        List<RenderedLessonDto> lessons = scheduleQueryService.searchForAggregation(
                effectiveDepartmentId, null, startDate, endDate
        );
        return summarize(lessons, RenderedLessonDto::classroomId, RenderedLessonDto::classroomName, ignored -> null);
    }

    @GetMapping("/departments")
    public List<WorkloadSummaryDto> departmentWorkload(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
            @RequestParam(required = false) Long departmentId
    ) {
        Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
        List<RenderedLessonDto> lessons = scheduleQueryService.searchForAggregation(
                effectiveDepartmentId, null, startDate, endDate
        );
        Map<Long, String> departments = departmentRepository.findAll().stream()
                .collect(Collectors.toMap(Department::getId, Department::getDepartmentName));
        Map<Long, List<TeacherDepartmentAssignment>> assignmentsByTeacher =
                assignmentsByTeacher(lessons, startDate, endDate);

        Map<Long, List<RenderedLessonDto>> byDepartment = lessons.stream()
                .collect(Collectors.groupingBy(lesson -> departmentIdAtLesson(
                        lesson,
                        assignmentsByTeacher
                ).orElse(0L)));

        return byDepartment.entrySet().stream()
                .map(entry -> summary(entry.getKey(), departments.getOrDefault(entry.getKey(), "Кафедра не указана"), entry.getKey(), entry.getValue()))
                .sorted(Comparator.comparing(WorkloadSummaryDto::name, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private List<WorkloadSummaryDto> summarizeTeacherWorkload(List<RenderedLessonDto> lessons,
                                                              LocalDate startDate,
                                                              LocalDate endDate) {
        Map<Long, List<TeacherDepartmentAssignment>> assignmentsByTeacher =
                assignmentsByTeacher(lessons, startDate, endDate);
        Map<TeacherDepartmentKey, List<RenderedLessonDto>> grouped = lessons.stream()
                .filter(lesson -> lesson.teacherId() != null)
                .collect(Collectors.groupingBy(
                        lesson -> new TeacherDepartmentKey(
                                lesson.teacherId(),
                                departmentIdAtLesson(lesson, assignmentsByTeacher).orElse(null)
                        ),
                        LinkedHashMap::new,
                        Collectors.toList()
                ));
        return grouped.entrySet().stream()
                .map(entry -> summary(
                        entry.getKey().teacherId(),
                        entry.getValue().get(0).teacherName(),
                        entry.getKey().departmentId(),
                        entry.getValue()
                ))
                .sorted(Comparator
                        .comparing(WorkloadSummaryDto::name, Comparator.nullsLast(String::compareTo))
                        .thenComparing(
                                WorkloadSummaryDto::departmentId,
                                Comparator.nullsLast(Long::compareTo)
                        ))
                .toList();
    }

    private Map<Long, List<TeacherDepartmentAssignment>> assignmentsByTeacher(
            List<RenderedLessonDto> lessons,
            LocalDate startDate,
            LocalDate endDate
    ) {
        Set<Long> teacherIds = lessons.stream()
                .map(RenderedLessonDto::teacherId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        return teacherDepartmentService.findAssignmentsForTeachersBetween(
                teacherIds,
                startDate,
                endDate
        );
    }

    private Optional<Long> departmentIdAtLesson(
            RenderedLessonDto lesson,
            Map<Long, List<TeacherDepartmentAssignment>> assignmentsByTeacher
    ) {
        if (lesson.teacherId() == null || lesson.date() == null) {
            return Optional.empty();
        }
        return teacherDepartmentService.resolveDepartmentIdAtDate(
                assignmentsByTeacher.getOrDefault(lesson.teacherId(), List.of()),
                lesson.date()
        );
    }

    @GetMapping("/time-slots")
    public List<WorkloadSummaryDto> timeSlotWorkload(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
            @RequestParam(required = false) Long departmentId
    ) {
        Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
        List<RenderedLessonDto> lessons = scheduleQueryService.searchForAggregation(
                effectiveDepartmentId, null, startDate, endDate
        );
        return summarize(lessons, RenderedLessonDto::timeSlotId,
                lesson -> lesson.timeSlotOrder() == null
                        ? "Пара"
                        : lesson.timeSlotOrder() + " пара (" + lesson.startTime() + " - " + lesson.endTime() + ")",
                ignored -> null);
    }

    @GetMapping("/free-classrooms")
    public List<ClassroomResponse> freeClassrooms(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
            @RequestParam Long timeSlotId
    ) {
        Long effectiveDepartmentId = effectiveDepartmentId(null);
        Set<Long> busyClassroomIds = scheduleQueryService.searchForAggregation(
                        effectiveDepartmentId, timeSlotId, date, date
                )
                .stream()
                .map(RenderedLessonDto::classroomId)
                .filter(Objects::nonNull)
                .collect(Collectors.toSet());
        return classroomRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED).stream()
                .filter(classroom -> Boolean.TRUE.equals(classroom.getIsAvailable()))
                .filter(classroom -> !busyClassroomIds.contains(classroom.getId()))
                .map(this::toClassroomResponse)
                .toList();
    }

    private List<WorkloadSummaryDto> summarize(List<RenderedLessonDto> lessons,
                                               Function<RenderedLessonDto, Long> idFn,
                                               Function<RenderedLessonDto, String> nameFn,
                                               Function<Long, Long> departmentFn) {
        Map<Long, List<RenderedLessonDto>> grouped = lessons.stream()
                .filter(lesson -> idFn.apply(lesson) != null)
                .collect(Collectors.groupingBy(idFn, LinkedHashMap::new, Collectors.toList()));
        return grouped.entrySet().stream()
                .map(entry -> summary(entry.getKey(), nameFn.apply(entry.getValue().get(0)), departmentFn.apply(entry.getKey()), entry.getValue()))
                .sorted(Comparator.comparing(WorkloadSummaryDto::name, Comparator.nullsLast(String::compareTo)))
                .toList();
    }

    private WorkloadSummaryDto summary(Long id, String name, Long departmentId, List<RenderedLessonDto> lessons) {
        return new WorkloadSummaryDto(
                id,
                name,
                departmentId,
                departmentName(departmentId),
                lessons.size(),
                lessons.size() * ACADEMIC_HOURS_PER_LESSON,
                lessons.stream()
                        .map(lesson -> lesson.date() + ":" + lesson.timeSlotId())
                        .distinct()
                        .count()
        );
    }

    private String departmentName(Long departmentId) {
        if (departmentId == null) {
            return null;
        }
        if (departmentId == 0L) {
            return "Кафедра не указана";
        }
        return departmentRepository.findById(departmentId)
                .map(Department::getDepartmentName)
                .orElse("Кафедра не найдена");
    }

    private ClassroomResponse toClassroomResponse(Classroom classroom) {
        return new ClassroomResponse(
                classroom.getId(),
                classroom.getName(),
                classroom.getCapacity(),
                classroom.getBuilding(),
                classroom.getFloor(),
                classroom.getIsAvailable(),
                classroom.getStatus(),
                classroom.getArchivedAt(),
                classroom.getArchiveReason(),
                new ArrayList<>(classroom.getEquipments())
        );
    }

    private Long effectiveDepartmentId(Long requestedDepartmentId) {
        var currentUser = AuthContext.getCurrentUser();
        if (currentUser == null) {
            throw new ResponseStatusException(
                    HttpStatus.UNAUTHORIZED,
                    "Не удалось определить текущего пользователя"
            );
        }
        if (currentUser.role() != Role.DEPARTMENT) {
            return requestedDepartmentId;
        }

        Long ownDepartmentId = currentUser.departmentId();
        if (ownDepartmentId == null) {
            throw new ResponseStatusException(
                    HttpStatus.FORBIDDEN,
                    "Для пользователя кафедры не указана кафедра"
            );
        }
        if (requestedDepartmentId != null && !Objects.equals(requestedDepartmentId, ownDepartmentId)) {
            throw new ResponseStatusException(
                    HttpStatus.FORBIDDEN,
                    "Нельзя просматривать нагрузку другой кафедры"
            );
        }
        return ownDepartmentId;
    }

    private record TeacherDepartmentKey(Long teacherId, Long departmentId) {
    }
}
