package ru.vyatsu.qr_access_admin.door.schedule.component;

import com.vaadin.flow.component.Composite;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.component.timepicker.TimePicker;
import com.vaadin.flow.data.binder.BeanValidationBinder;
import lombok.Setter;
import org.springframework.util.Assert;
import ru.vyatsu.qr_access_admin.door.schedule.entity.ScheduleEntity;
import ru.vyatsu.qr_access_admin.door.schedule.entity.ScheduleRepository;

import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

import static com.vaadin.flow.component.icon.VaadinIcon.ARROW_LEFT;
import static com.vaadin.flow.component.icon.VaadinIcon.ARROW_RIGHT;

public class ScheduleEditor extends Composite<VerticalLayout> {

    private Week chosenWeek;
    @Setter
    private String currentDoorId;
    private Map<LocalDate, ScheduleEntity> newSchedule = new HashMap<>();
    private Set<Week> queriedWeeks = new HashSet<>();
    private final ScheduleRepository repository;
    private final VerticalLayout daysEditorWrapper = new VerticalLayout();
    private final Div currentWeek;

    public ScheduleEditor(ScheduleRepository repository) {
        this.repository = repository;
        this.chosenWeek = Week.current();
        currentWeek = new Div(chosenWeek.toString());
        currentWeek.getStyle().set("align-content", "center");
        refreshScheduleView();

        var weekChanger = new HorizontalLayout();

        var rightArrowButton = new Button(ARROW_RIGHT.create(), e -> {
            daysEditorWrapper.removeAll();
            this.chosenWeek = new Week(this.chosenWeek.dateMonday.plusDays(7), this.chosenWeek.dateSunday.plusDays(7));
            currentWeek.setText(chosenWeek.toString());
            List<ScheduleEntity> foundSchedule;
            if (queriedWeeks.contains(chosenWeek)) {
                foundSchedule = getEditedScheduleEntriesByWeek(chosenWeek);
            } else {
                foundSchedule = repository.findByDoorIdAndDateGreaterThanEqualAndDateLessThanEqualOrderByDateAsc(currentDoorId, chosenWeek.dateMonday, chosenWeek.dateSunday);
                fillAbsentDays(foundSchedule, currentDoorId, chosenWeek);
            }
            foundSchedule.forEach(se -> {
                HorizontalLayout scheduleEntry = new HorizontalLayout();
                var entryDate = new Span(se.getDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - E")));
                entryDate.getStyle().set("align-content", "center");
                var timePickerFrom = new TimePicker("Время начала работы");
                var timePickerTo = new TimePicker("Время окончания работы");
                var binder = new BeanValidationBinder<>(ScheduleEntity.class);
                binder.setBean(se);
                binder.forField(timePickerFrom).bind("startTime");
                binder.forField(timePickerTo).bind("endTime");
                newSchedule.put(se.getDate(), se);
                scheduleEntry.add(entryDate, timePickerFrom, timePickerTo);
                daysEditorWrapper.add(scheduleEntry);
            });
            queriedWeeks.add(chosenWeek);
        });
        var leftArrowButton = new Button(ARROW_LEFT.create(), e -> {
            daysEditorWrapper.removeAll();
            this.chosenWeek = new Week(this.chosenWeek.dateMonday.minusDays(7), this.chosenWeek.dateSunday.minusDays(7));
            currentWeek.setText(chosenWeek.toString());
            List<ScheduleEntity> foundSchedule;
            if (queriedWeeks.contains(chosenWeek)) {
                foundSchedule = getEditedScheduleEntriesByWeek(chosenWeek);
            } else {
                foundSchedule = repository.findByDoorIdAndDateGreaterThanEqualAndDateLessThanEqualOrderByDateAsc(currentDoorId, chosenWeek.dateMonday, chosenWeek.dateSunday);
                fillAbsentDays(foundSchedule, currentDoorId, chosenWeek);
            }
            foundSchedule.forEach(se -> {
                HorizontalLayout scheduleEntry = new HorizontalLayout();
                var entryDate = new Span(se.getDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - E")));
                entryDate.getStyle().set("align-content", "center");
                var timePickerFrom = new TimePicker("Время начала работы");
                var timePickerTo = new TimePicker("Время окончания работы");
                var binder = new BeanValidationBinder<>(ScheduleEntity.class);
                binder.setBean(se);
                binder.forField(timePickerFrom).bind("startTime");
                binder.forField(timePickerTo).bind("endTime");
                newSchedule.put(se.getDate(), se);
                scheduleEntry.add(entryDate, timePickerFrom, timePickerTo);
                daysEditorWrapper.add(scheduleEntry);
            });
            queriedWeeks.add(chosenWeek);
        });
        weekChanger.add(leftArrowButton, currentWeek, rightArrowButton);

        getContent().add(weekChanger, daysEditorWrapper);
    }

    public void refreshScheduleView() {
        this.chosenWeek = Week.current();
        currentWeek.setText(chosenWeek.toString());
        newSchedule = new HashMap<>();
        queriedWeeks = new HashSet<>();
        daysEditorWrapper.removeAll();
        List<ScheduleEntity> schedule = repository.findByDoorIdAndDateGreaterThanEqualAndDateLessThanEqualOrderByDateAsc(currentDoorId, chosenWeek.dateMonday, chosenWeek.dateSunday);
        fillAbsentDays(schedule, currentDoorId, chosenWeek);
        schedule.forEach(se -> {
            HorizontalLayout scheduleEntry = new HorizontalLayout();
            var entryDate = new Span(se.getDate().format(DateTimeFormatter.ofPattern("dd.MM.yyyy - E")));
            entryDate.getStyle().set("align-content", "center");
            var timePickerFrom = new TimePicker("Время начала работы");
            var timePickerTo = new TimePicker("Время окончания работы");
            var binder = new BeanValidationBinder<>(ScheduleEntity.class);
            binder.setBean(se);
            binder.forField(timePickerFrom).bind("startTime");
            binder.forField(timePickerTo).bind("endTime");
            newSchedule.put(se.getDate(), se);
            scheduleEntry.add(entryDate, timePickerFrom, timePickerTo);
            daysEditorWrapper.add(scheduleEntry);
        });
        queriedWeeks.add(chosenWeek);
    }

    public Collection<ScheduleEntity> getNewSchedule() {
        return Collections.unmodifiableCollection(newSchedule.values());
    }

    private List<ScheduleEntity> getEditedScheduleEntriesByWeek(Week chosenWeek) {
        var i = chosenWeek.iterator();
        List<ScheduleEntity> result = new ArrayList<>();
        while (i.hasNext()) {
            result.add(newSchedule.get(i.next()));
        }
        return result;
    }

    private void fillAbsentDays(List<ScheduleEntity> schedule, String currentDoorId, Week week) {
        Set<LocalDate> existDates = schedule.stream()
                .map(ScheduleEntity::getDate)
                .collect(Collectors.toSet());
        Iterator<LocalDate> iter = week.iterator();
        for (int i = 0; iter.hasNext(); i++) {
            LocalDate currDate = iter.next();
            if (!existDates.contains(currDate)) {
                ScheduleEntity newEntity = new ScheduleEntity();
                newEntity.setId(UUID.randomUUID().toString());
                newEntity.setDoorId(currentDoorId);
                newEntity.setStartTime(LocalTime.MIN);
                newEntity.setEndTime(LocalTime.MAX);
                newEntity.setDate(currDate);
                schedule.add(i, newEntity);
            }
        }
    }

    record Week(LocalDate dateMonday, LocalDate dateSunday) implements Iterable<LocalDate> {
        public Week {
            Objects.requireNonNull(dateMonday, "dateMonday cannot be null");
            Objects.requireNonNull(dateSunday, "dateSunday cannot be null");

            Assert.state(dateMonday.getDayOfWeek().equals(DayOfWeek.MONDAY), "dateMonday must be Monday");
            Assert.state(dateSunday.getDayOfWeek().equals(DayOfWeek.SUNDAY), "dateSunday must be Sunday");

            Assert.state(dateSunday.isAfter(dateMonday), "dateMonday should be less than dateSunday");
        }

        public static Week current() {
            LocalDate now = LocalDate.now();
            LocalDate monday = now.minusDays(now.getDayOfWeek().getValue() - 1);
            LocalDate sunday = now.plusDays(7 - now.getDayOfWeek().getValue());
            return new Week(monday, sunday);
        }

        @Override
        public String toString() {
            DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("dd.MM.yyyy");
            return dateMonday.format(dateFormat).concat(" - ").concat(dateSunday.format(dateFormat));
        }

        @Override
        public Iterator<LocalDate> iterator() {
            return new Iterator<>() {
                private LocalDate current = dateMonday.minusDays(1);

                @Override
                public boolean hasNext() {
                    return current.isBefore(dateSunday);
                }

                @Override
                public LocalDate next() {
                    return current = current.plusDays(1);
                }
            };
        }
    }
}
