package com.magistr.app.controller;

import com.magistr.app.config.auth.RequireRoles;
import com.magistr.app.dto.TimeSlotDateAssignmentDto;
import com.magistr.app.dto.TimeSlotDto;
import com.magistr.app.dto.TimeSlotScopeDto;
import com.magistr.app.model.TimeSlot;
import com.magistr.app.model.TimeSlotDateAssignment;
import com.magistr.app.model.TimeSlotScope;
import com.magistr.app.model.Role;
import com.magistr.app.repository.TimeSlotDateAssignmentRepository;
import com.magistr.app.repository.TimeSlotRepository;
import com.magistr.app.repository.TimeSlotScopeRepository;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.Duration;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;

@RestController
@RequestMapping("/api/admin/time-slots")
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
public class TimeSlotAdminController {

    private static final String APPLY_MODE_DEFAULT = "DEFAULT";
    private static final String APPLY_MODE_WEEKDAY = "WEEKDAY";
    private static final String APPLY_MODE_MANUAL = "MANUAL";

    private final TimeSlotRepository timeSlotRepository;
    private final TimeSlotScopeRepository timeSlotScopeRepository;
    private final TimeSlotDateAssignmentRepository dateAssignmentRepository;

    public TimeSlotAdminController(TimeSlotRepository timeSlotRepository,
                                   TimeSlotScopeRepository timeSlotScopeRepository,
                                   TimeSlotDateAssignmentRepository dateAssignmentRepository) {
        this.timeSlotRepository = timeSlotRepository;
        this.timeSlotScopeRepository = timeSlotScopeRepository;
        this.dateAssignmentRepository = dateAssignmentRepository;
    }

    @GetMapping
    public List<TimeSlotDto> getAll() {
        return timeSlotRepository.findAllByOrderByTimeSlotScopeDisplayOrderAscOrderNumberAscStartTimeAsc().stream()
                .map(this::toDto)
                .toList();
    }

    @GetMapping("/effective")
    public ResponseEntity<?> getEffective(@RequestParam LocalDate date) {
        return ResponseEntity.ok(effectiveSlots(date).stream().map(this::toDto).toList());
    }

    @GetMapping("/scopes")
    public List<TimeSlotScopeDto> getScopes() {
        return timeSlotScopeRepository.findAllByOrderByDisplayOrderAscNameAsc().stream()
                .map(this::toScopeDto)
                .toList();
    }

    @PostMapping("/scopes")
    public ResponseEntity<?> createScope(@RequestBody TimeSlotScopeDto request) {
        if (request.name() == null || request.name().isBlank()) {
            return ResponseEntity.badRequest().body(Map.of("message", "Название сетки обязательно"));
        }

        TimeSlotScope scope = new TimeSlotScope();
        scope.setCode("manual_" + UUID.randomUUID().toString().replace("-", ""));
        scope.setName(request.name().trim());
        scope.setApplyMode(APPLY_MODE_MANUAL);
        scope.setDayOfWeek(null);
        scope.setSystemScope(false);
        scope.setDisplayOrder(nextManualDisplayOrder());
        return ResponseEntity.ok(toScopeDto(timeSlotScopeRepository.save(scope)));
    }

    @PutMapping("/scopes/{id}")
    public ResponseEntity<?> updateScope(@PathVariable Long id, @RequestBody TimeSlotScopeDto request) {
        TimeSlotScope scope = timeSlotScopeRepository.findById(id).orElse(null);
        if (scope == null) {
            return ResponseEntity.notFound().build();
        }
        if (Boolean.TRUE.equals(scope.getSystemScope())) {
            return ResponseEntity.badRequest().body(Map.of("message", "Системную сетку нельзя изменять"));
        }
        if (request.name() == null || request.name().isBlank()) {
            return ResponseEntity.badRequest().body(Map.of("message", "Название сетки обязательно"));
        }
        scope.setName(request.name().trim());
        return ResponseEntity.ok(toScopeDto(timeSlotScopeRepository.save(scope)));
    }

    @DeleteMapping("/scopes/{id}")
    public ResponseEntity<?> deleteScope(@PathVariable Long id) {
        TimeSlotScope scope = timeSlotScopeRepository.findById(id).orElse(null);
        if (scope == null) {
            return ResponseEntity.notFound().build();
        }
        if (Boolean.TRUE.equals(scope.getSystemScope())) {
            return ResponseEntity.badRequest().body(Map.of("message", "Системную сетку нельзя удалить"));
        }
        timeSlotScopeRepository.delete(scope);
        return ResponseEntity.ok(Map.of("message", "Сетка времени удалена"));
    }

    @GetMapping("/date-assignments")
    public List<TimeSlotDateAssignmentDto> getDateAssignments(@RequestParam(required = false) LocalDate startDate,
                                                              @RequestParam(required = false) LocalDate endDate) {
        List<TimeSlotDateAssignment> assignments = startDate != null && endDate != null
                ? dateAssignmentRepository.findByDateBetweenOrderByDateAsc(startDate, endDate)
                : dateAssignmentRepository.findAllByOrderByDateAsc();
        return assignments.stream().map(this::toAssignmentDto).toList();
    }

    @PostMapping("/date-assignments")
    public ResponseEntity<?> saveDateAssignment(@RequestBody TimeSlotDateAssignmentDto request) {
        if (request.date() == null) {
            return ResponseEntity.badRequest().body(Map.of("message", "Дата обязательна"));
        }
        if (request.scopeId() == null) {
            return ResponseEntity.badRequest().body(Map.of("message", "Сетка времени обязательна"));
        }
        TimeSlotScope scope = timeSlotScopeRepository.findById(request.scopeId()).orElse(null);
        if (scope == null) {
            return ResponseEntity.badRequest().body(Map.of("message", "Сетка времени не найдена"));
        }
        if (!APPLY_MODE_MANUAL.equals(scope.getApplyMode())) {
            return ResponseEntity.badRequest().body(Map.of("message", "Вручную можно применять только пользовательские сетки"));
        }

        TimeSlotDateAssignment assignment = dateAssignmentRepository.findByDate(request.date())
                .orElseGet(TimeSlotDateAssignment::new);
        assignment.setDate(request.date());
        assignment.setTimeSlotScope(scope);
        return ResponseEntity.ok(toAssignmentDto(dateAssignmentRepository.save(assignment)));
    }

    @DeleteMapping("/date-assignments/{id}")
    public ResponseEntity<?> deleteDateAssignment(@PathVariable Long id) {
        if (!dateAssignmentRepository.existsById(id)) {
            return ResponseEntity.notFound().build();
        }
        dateAssignmentRepository.deleteById(id);
        return ResponseEntity.ok(Map.of("message", "Ручное назначение удалено"));
    }

    @PostMapping
    public ResponseEntity<?> create(@RequestBody TimeSlotDto request) {
        String validationError = validate(request);
        if (validationError != null) {
            return ResponseEntity.badRequest().body(Map.of("message", validationError));
        }
        if (hasDuplicate(request, null)) {
            return ResponseEntity.badRequest().body(Map.of("message", "В выбранной сетке уже есть слот с таким номером пары"));
        }

        TimeSlot timeSlot = new TimeSlot();
        apply(timeSlot, request);
        return ResponseEntity.ok(toDto(timeSlotRepository.save(timeSlot)));
    }

    @PutMapping("/{id}")
    public ResponseEntity<?> update(@PathVariable Long id, @RequestBody TimeSlotDto request) {
        TimeSlot timeSlot = timeSlotRepository.findById(id).orElse(null);
        if (timeSlot == null) {
            return ResponseEntity.notFound().build();
        }

        String validationError = validate(request);
        if (validationError != null) {
            return ResponseEntity.badRequest().body(Map.of("message", validationError));
        }
        if (hasDuplicate(request, id)) {
            return ResponseEntity.badRequest().body(Map.of("message", "В выбранной сетке уже есть слот с таким номером пары"));
        }

        apply(timeSlot, request);
        return ResponseEntity.ok(toDto(timeSlotRepository.save(timeSlot)));
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<?> delete(@PathVariable Long id) {
        if (!timeSlotRepository.existsById(id)) {
            return ResponseEntity.notFound().build();
        }
        try {
            timeSlotRepository.deleteById(id);
            return ResponseEntity.ok(Map.of("message", "Временной слот удалён"));
        } catch (DataIntegrityViolationException e) {
            return ResponseEntity.badRequest().body(Map.of("message", "Нельзя удалить слот, который используется в правилах расписания"));
        }
    }

    private String validate(TimeSlotDto request) {
        if (request.orderNumber() == null || request.orderNumber() <= 0) {
            return "Номер пары должен быть больше нуля";
        }
        if (request.scopeId() == null || timeSlotScopeRepository.findById(request.scopeId()).isEmpty()) {
            return "Выберите сетку времени";
        }
        if (request.startTime() == null || request.endTime() == null) {
            return "Время начала и окончания обязательно";
        }
        if (!request.startTime().isBefore(request.endTime())) {
            return "Время начала должно быть раньше времени окончания";
        }
        return null;
    }

    private void apply(TimeSlot timeSlot, TimeSlotDto request) {
        timeSlot.setOrderNumber(request.orderNumber());
        timeSlot.setTimeSlotScope(timeSlotScopeRepository.findById(request.scopeId())
                .orElseThrow(() -> new IllegalArgumentException("Сетка времени не найдена")));
        timeSlot.setStartTime(request.startTime());
        timeSlot.setEndTime(request.endTime());
        int duration = request.durationMinutes() != null && request.durationMinutes() > 0
                ? request.durationMinutes()
                : (int) Duration.between(request.startTime(), request.endTime()).toMinutes();
        timeSlot.setDurationMinutes(duration);
    }

    private boolean hasDuplicate(TimeSlotDto request, Long currentId) {
        return timeSlotRepository.findByTimeSlotScopeIdAndOrderNumber(request.scopeId(), request.orderNumber())
                .filter(existing -> currentId == null || !existing.getId().equals(currentId))
                .isPresent();
    }

    private List<TimeSlot> effectiveSlots(LocalDate date) {
        TimeSlotScope defaultScope = defaultScope();
        TimeSlotScope effectiveScope = effectiveScope(date);
        List<TimeSlot> defaultSlots = timeSlotRepository.findAllByTimeSlotScopeIdOrderByOrderNumberAsc(defaultScope.getId());
        if (effectiveScope.getId().equals(defaultScope.getId())) {
            return defaultSlots;
        }

        Map<Integer, TimeSlot> slotsByOrder = new LinkedHashMap<>();
        defaultSlots.forEach(slot -> slotsByOrder.put(slot.getOrderNumber(), slot));
        timeSlotRepository.findAllByTimeSlotScopeIdOrderByOrderNumberAsc(effectiveScope.getId())
                .forEach(slot -> slotsByOrder.put(slot.getOrderNumber(), slot));
        return new ArrayList<>(slotsByOrder.values());
    }

    private TimeSlotScope effectiveScope(LocalDate date) {
        return dateAssignmentRepository.findByDate(date)
                .map(TimeSlotDateAssignment::getTimeSlotScope)
                .or(() -> timeSlotScopeRepository.findByApplyModeAndDayOfWeek(
                        APPLY_MODE_WEEKDAY,
                        date.getDayOfWeek().getValue()))
                .orElseGet(this::defaultScope);
    }

    private TimeSlotScope defaultScope() {
        return timeSlotScopeRepository.findFirstByApplyModeOrderByDisplayOrderAsc(APPLY_MODE_DEFAULT)
                .orElseThrow(() -> new IllegalStateException("Базовая сетка времени не настроена"));
    }

    private int nextManualDisplayOrder() {
        return timeSlotScopeRepository.findAllByOrderByDisplayOrderAscNameAsc().stream()
                .map(TimeSlotScope::getDisplayOrder)
                .filter(value -> value != null)
                .max(Integer::compareTo)
                .orElse(100) + 10;
    }

    private TimeSlotDto toDto(TimeSlot timeSlot) {
        TimeSlotScope scope = timeSlot.getTimeSlotScope();
        return new TimeSlotDto(
                timeSlot.getId(),
                timeSlot.getOrderNumber(),
                scope == null ? null : scope.getId(),
                scope == null ? null : scope.getName(),
                scope == null ? null : scope.getApplyMode(),
                scope == null ? null : scope.getDayOfWeek(),
                timeSlot.getStartTime(),
                timeSlot.getEndTime(),
                timeSlot.getDurationMinutes()
        );
    }

    private TimeSlotScopeDto toScopeDto(TimeSlotScope scope) {
        return new TimeSlotScopeDto(
                scope.getId(),
                scope.getCode(),
                scope.getName(),
                scope.getApplyMode(),
                scope.getDayOfWeek(),
                scope.getSystemScope(),
                scope.getDisplayOrder()
        );
    }

    private TimeSlotDateAssignmentDto toAssignmentDto(TimeSlotDateAssignment assignment) {
        TimeSlotScope scope = assignment.getTimeSlotScope();
        return new TimeSlotDateAssignmentDto(
                assignment.getId(),
                assignment.getDate(),
                scope.getId(),
                scope.getName()
        );
    }
}
