/*
 * Queue - A Queueing system that can be used to handle labs in higher education
 * Copyright (C) 2016-2024  Delft University of Technology
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package nl.tudelft.queue.service;

import static java.time.LocalDateTime.now;
import static java.util.Collections.emptySet;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;

import jakarta.transaction.Transactional;
import nl.tudelft.labracore.api.EditionControllerApi;
import nl.tudelft.labracore.api.ModuleControllerApi;
import nl.tudelft.labracore.api.PersonControllerApi;
import nl.tudelft.labracore.api.SessionControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.labracore.lib.security.user.Person;
import nl.tudelft.librador.dto.DTOConverter;
import nl.tudelft.queue.cache.AssignmentCacheManager;
import nl.tudelft.queue.cache.EditionCacheManager;
import nl.tudelft.queue.cache.EditionCollectionCacheManager;
import nl.tudelft.queue.cache.ModuleCacheManager;
import nl.tudelft.queue.cache.SessionCacheManager;
import nl.tudelft.queue.csv.*;
import nl.tudelft.queue.dto.create.QueueSessionCreateDTO;
import nl.tudelft.queue.dto.patch.QueueSessionPatchDTO;
import nl.tudelft.queue.dto.patch.embeddables.LabRequestConstraintsPatchDTO;
import nl.tudelft.queue.dto.view.CalendarEntryViewDTO;
import nl.tudelft.queue.dto.view.QueueEditionDetailsDTO;
import nl.tudelft.queue.dto.view.requests.LabRequestViewDTO;
import nl.tudelft.queue.dto.view.requests.SelectionRequestViewDTO;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.QueueSession;
import nl.tudelft.queue.model.Request;
import nl.tudelft.queue.model.TimeSlot;
import nl.tudelft.queue.model.constraints.ClusterConstraint;
import nl.tudelft.queue.model.constraints.ModuleDivisionConstraint;
import nl.tudelft.queue.model.embeddables.AllowedRequest;
import nl.tudelft.queue.model.embeddables.LabRequestConstraints;
import nl.tudelft.queue.model.enums.OnlineMode;
import nl.tudelft.queue.model.enums.RequestType;
import nl.tudelft.queue.model.enums.SelectionProcedure;
import nl.tudelft.queue.model.labs.AbstractSlottedLab;
import nl.tudelft.queue.model.labs.CapacitySession;
import nl.tudelft.queue.model.labs.ExamLab;
import nl.tudelft.queue.model.labs.Lab;
import nl.tudelft.queue.model.labs.RegularLab;
import nl.tudelft.queue.model.misc.Presentation;
import nl.tudelft.queue.repository.LabRepository;
import nl.tudelft.queue.repository.LabRequestConstraintRepository;
import nl.tudelft.queue.repository.PresentationRepository;
import nl.tudelft.queue.repository.QueueSessionRepository;

@Service
public class LabService {

	@Autowired
	private LabRequestConstraintRepository labRequestConstraintRepository;

	@Autowired
	private SessionControllerApi sApi;

	@Autowired
	private EditionCacheManager eCache;

	@Autowired
	private EditionCollectionCacheManager ecCache;

	@Autowired
	private SessionCacheManager sCache;

	@Autowired
	private AssignmentCacheManager aCache;

	@Autowired
	private ModuleCacheManager mCache;

	@Autowired
	private EditionService es;

	@Autowired
	private EditionControllerApi eApi;

	@Autowired
	private FileService fs;

	@Autowired
	private RequestTableService rts;

	@Autowired
	private TimeSlotService tss;

	@Autowired
	private LabRepository lr;

	@Autowired
	private QueueSessionRepository qsr;

	@Autowired
	private PresentationRepository pr;

	@Autowired
	private CapacitySessionService css;

	@Autowired
	private RoleDTOService rService;

	@Autowired
	private ModuleControllerApi mApi;

	@Autowired
	private PersonControllerApi pca;

	@Autowired
	private DTOConverter converter;

	@Autowired
	private SessionService sessionService;
	@Autowired
	private SessionCacheManager sessionCache;

	@Autowired
	private LabRequestConstraintService labRequestConstraintService;

	public enum SessionType {
		REGULAR,
		SHARED
	}

	/**
	 * Sets the organizational unit that the given lab relates to in the given model. This could either be an
	 * Edition or an EditionCollection.
	 *
	 * @param lab   The lab from which to extract the organizational unit.
	 * @param model The model to set for Thymeleaf resolution.
	 */
	public void setOrganizationInModel(QueueSession<?> lab, Model model) {
		var session = sessionService.getCoreSession(lab.getSession());
		setOrganizationInModel(session, model);
	}

	/**
	 * Sets the organizational unit that the given session relates to in the given model. This could either be
	 * an Edition or an EditionCollection.
	 *
	 * @param session The session from which to extract the organizational unit.
	 * @param model   The model to set for Thymeleaf resolution.
	 */
	public void setOrganizationInModel(SessionDetailsDTO session, Model model) {
		model.addAttribute("edition",
				(session.getEdition() != null)
						? es.queueEditionDTO(eCache.getRequired(session.getEdition().getId()),
								QueueEditionDetailsDTO.class)
						: null);
		model.addAttribute("ec",
				(session.getEditionCollection() != null)
						? ecCache.getRequired(session.getEditionCollection().getId())
						: null);
	}

	/**
	 * Converts the given session to a CSV with a list of the requests in that lab.
	 *
	 * @param  qSession    The session to convert to a CSV.
	 * @return             The resource representing the CSV file.
	 * @throws IOException when something goes wrong while reading or writing the file.
	 */
	public Resource sessionToCsv(QueueSession<?> qSession) throws IOException {
		String[] header = new String[0];
		if (qSession instanceof CapacitySession) {
			header = SelectionRequestViewDTO.csvHeader();
		} else if (qSession instanceof Lab) {
			header = LabRequestViewDTO.csvHeader();
		}
		String[] finalHeader = header;
		String name = sessionService.getCoreSession(qSession.getSession()).getName();
		var requests = rts.convertRequestsToView(qSession.getRequests());
		return fs.writeTempResource("session-" + name + "-" + requests.hashCode() + ".csv",
				() -> CsvHelper.writeCsvValues(finalHeader, requests, ','));
	}

	public List<Resource> sessionDTOToCsv(SessionSummaryDTO session) throws IOException {
		var sessions = qsr.findAllBySessions(List.of(session.getId()));
		List<Resource> list = new ArrayList<>();
		for (QueueSession<?> queueSession : sessions) {
			Resource resource = sessionToCsv(queueSession);
			list.add(resource);
		}
		return list;
	}

	/**
	 * Checks whether the given session contains the room with the given id.
	 *
	 * @param  session The session that is to be checked for rooms.
	 * @param  roomId  The id of the room to check for.
	 * @return         Whether the given room is in the given session.
	 */
	public boolean containsRoom(SessionDetailsDTO session, Long roomId) {
		return session.getRooms().stream()
				.anyMatch(r -> Objects.equals(r.getId(), roomId));
	}

	/**
	 * Checks whether the given lab allows the assignment with the given id.
	 *
	 * @param  lab        The lab that needs to be checked.
	 * @param  assignment The assignment that needs to be checked for.
	 * @return            Whether the given assignment is active in the given lab.
	 */
	public boolean containsAssignment(Lab lab, Long assignment) {
		return lab.getAllowedRequests().stream()
				.anyMatch(ar -> Objects.equals(ar.getAssignment(), assignment));
	}

	/**
	 * Checks whether the given lab allows a request with the given assignment and request type.
	 *
	 * @param  lab        The lab that needs to be checked.
	 * @param  assignment The assignment that needs to be checked for.
	 * @param  type       The type that needs to be checked for.
	 * @return            Whether the given assignment and request type combination are allowed in the lab.
	 */
	public boolean containsAllowedRequest(Lab lab, Long assignment, RequestType type) {
		return lab.getAllowedRequests().contains(new AllowedRequest(assignment, type));
	}

	@Transactional
	public <D extends QueueSession<?>> List<D> createSessions(QueueSessionCreateDTO<D> dto, Long ouId,
			SessionType sessionType) {
		ArrayList<D> sessions = new ArrayList<>();
		sessions.add(createOneSession(dto, ouId, sessionType));

		if (dto.getRepeatForXWeeks() != null && dto.getRepeatForXWeeks() > 0) {
			for (int i = 0; i < dto.getRepeatForXWeeks(); i++) {
				dto.plusWeeks(1L);
				sessions.add(createOneSession(dto, ouId, sessionType));
			}
		}

		return sessions;
	}

	/**
	 * Creates a new lab using the create-DTO filled in when a user submitted the 'Create a new lab' form.
	 * Additional information to this method are the type of organizational unit that the lab related to and
	 * the ID of said organizational unit.
	 *
	 * @param  dto         The create DTO that was submitted by a user through the create page.
	 * @param  ouId        The ID of the organizational unit to create a lab for.
	 * @param  sessionType The type of session that is getting created (either a regular/single session or a
	 *                     shared one).
	 * @return             The created lab.
	 */
	@Transactional
	public <D extends QueueSession<?>> D createOneSession(QueueSessionCreateDTO<D> dto, Long ouId,
			SessionType sessionType) {
		D lab = converter.apply(dto);

		// Get the assignments and rooms as ID DTOs.
		var assignments = dto.assignmentIdDTOs();
		var rooms = dto.roomIdDTOs();

		// Add a session backing up this lab in Labracore.
		switch (sessionType) {
			case REGULAR -> lab.setSession(addSingleSession(dto, ouId, assignments, rooms));
			case SHARED -> lab.setSession(addSharedSession(dto, ouId, assignments, rooms));
		}

		// Save the lab.
		lab = qsr.save(lab);

		// Create the constraints for the created lab.
		lab.getConstraints().setLab(lab);
		labRequestConstraintRepository.saveAll(lab.getConstraints().toList());

		return lab;
	}

	/**
	 * Updates the information of a lab by updating the Queue specific Lab configurations and updating the
	 * session bound to the lab in Labracore.
	 *
	 * @param dto     The dto containing information on how to change the lab.
	 * @param session The lab that is to be edited.
	 */
	@Transactional
	public <D extends QueueSession<?>> void updateSession(QueueSessionPatchDTO<D> dto, D session) {
		// Get the assignments and rooms as ID DTOs.
		Set<AssignmentIdDTO> assignments = dto.assignmentIdDTOs() == null ? null
				: new HashSet<>(dto.assignmentIdDTOs());
		Set<RoomIdDTO> rooms = dto.roomIdDTOs() == null ? null : new HashSet<>(dto.roomIdDTOs());

		// Patch the session on the Labracore side of things.
		sApi.patchSession(new SessionPatchDTO()
				.name(dto.getName())
				.start((dto.getSlot() != null) ? dto.getSlot().getOpensAt() : null)
				.end((dto.getSlot() != null) ? dto.getSlot().getClosesAt() : null)
				.assignments(assignments)
				.rooms(rooms), session.getSession()).block();
		session.setExtraInfo(dto.getExtraInfo());

		converter.apply(dto, session);
		updateConstraints(session, dto.getConstraints(), session.getConstraints());
	}

	private void updateConstraints(QueueSession<?> session, LabRequestConstraintsPatchDTO patch,
			LabRequestConstraints constraints) {
		if (CollectionUtils.isEmpty(patch.getClusterConstraint().getClusters())) {
			if (constraints.getClusterConstraint() != null) {
				labRequestConstraintRepository.delete(constraints.getClusterConstraint());
				constraints.setClusterConstraint(null);
			}
		} else if (constraints.getClusterConstraint() == null) {
			constraints.setClusterConstraint(ClusterConstraint.builder().session(session)
					.clusters(patch.getClusterConstraint().getClusters()).build());
		} else {
			converter.apply(patch.getClusterConstraint(), constraints.getClusterConstraint());
		}

		if (CollectionUtils.isEmpty(patch.getModuleDivisionConstraint().getDivisions())) {
			if (constraints.getModuleDivisionConstraint() != null) {
				labRequestConstraintRepository.delete(constraints.getModuleDivisionConstraint());
				constraints.setModuleDivisionConstraint(null);
			}
		} else if (constraints.getModuleDivisionConstraint() == null) {
			constraints.setModuleDivisionConstraint(ModuleDivisionConstraint.builder()
					.session(session).divisions(patch.getModuleDivisionConstraint().getDivisions()).build());
		} else {
			converter.apply(patch.getModuleDivisionConstraint(), constraints.getModuleDivisionConstraint());
		}
	}

	public void updateCapacitySessionRequests(CapacitySession session,
			SelectionProcedure oldProcedure) {
		if (!session.getCapacitySessionConfig().getProcedure().equals(
				oldProcedure)) {
			css.resetRequests(session);
		}
	}

	/**
	 * Adds a regular session in Labracore to represent the Lab. This session is created every time a new lab
	 * gets created, just to back a Queue lab with a core session.
	 *
	 * @param  dto         The DTO used for creating the lab, containing information about the intended
	 *                     session.
	 * @param  editionId   The id of the edition to create this regular lab in.
	 * @param  assignments The assignments that are to be registered for the session.
	 * @param  rooms       The rooms that are to be registered for the session.
	 * @return             The id of the created session.
	 */
	private Long addSingleSession(QueueSessionCreateDTO<?> dto, Long editionId,
			List<AssignmentIdDTO> assignments, List<RoomIdDTO> rooms) {
		return sApi.addSingleSession(new SingleSessionCreateDTO()
				.name(dto.getName())
				.description("Queue lab session")
				.start(dto.getSlot().getOpensAt())
				.endTime(dto.getSlot().getClosesAt())
				.assignments(assignments)
				.rooms(rooms)
				.edition(new EditionIdDTO().id(editionId)))
				.block();
	}

	/**
	 * Adds a shared session in Labracore to represent the Lab. This session is created every time a new lab
	 * gets created, just to back a Queue lab with a core session.
	 *
	 * @param  dto                 The DTO used for creating the lab, containing information about the
	 *                             intended session.
	 * @param  editionCollectionId The id of the edition collection to create this shared lab in.
	 * @param  assignments         The assignments that are to be registered for the session.
	 * @param  rooms               The rooms that are to be registered for the session.
	 * @return                     The id of the created session.
	 */
	private Long addSharedSession(QueueSessionCreateDTO<?> dto, Long editionCollectionId,
			List<AssignmentIdDTO> assignments, List<RoomIdDTO> rooms) {
		return sApi.addSharedSession(new SharedSessionCreateDTO()
				.name(dto.getName())
				.description("Queue lab session")
				.start(dto.getSlot().getOpensAt())
				.endTime(dto.getSlot().getClosesAt())
				.assignments(assignments)
				.rooms(rooms)
				.editionCollection(new EditionCollectionIdDTO().id(editionCollectionId)))
				.block();
	}

	/**
	 * Converts labs to {@link CalendarEntryViewDTO} to be rendered in a calendar.
	 *
	 * @param  labs The labs to be converted.
	 * @return      A list of labs
	 */
	public List<CalendarEntryViewDTO> convertToCalendarEntries(List<Lab> labs) {
		return labs.stream()
				.map(lab -> {
					CalendarEntryViewDTO entry = converter.convert(lab, CalendarEntryViewDTO.class);
					entry.setSession(sessionCache.getRequired(lab.getSession()));
					return entry;
				})
				.toList();
	}

	/**
	 * Get all the labs which are taking place within a certain period.
	 *
	 * @param  start Start of the period for which to retrieve all the labs.
	 * @param  end   End of the period for which to retrieve all the labs.
	 * @return       A list of labs which are all occurring within the specified period.
	 */
	public List<Lab> getAllLabsWithinPeriod(LocalDateTime start, LocalDateTime end) {
		var editions = eCache
				.getAndIgnoreMissing(Objects.requireNonNull(eApi.getAllEditionsActiveDuringPeriod(start, end)
						.map(EditionSummaryDTO::getId).collectList().block()));
		var sessions = editions.stream().map(EditionDetailsDTO::getSessions).flatMap(List::stream)
				.toList();

		return lr.findAllBySessions(
				sessions.stream().map(SessionSummaryDTO::getId).collect(Collectors.toList()));
	}

	/**
	 * Count all the ongoing labs from a list of labs.
	 *
	 * @param  labs The labs to count for.
	 * @return      The number of labs which are currenlty open.
	 */
	@Transactional
	public long countOngoingLabs(List<Lab> labs) {
		LocalDateTime now = LocalDateTime.now();
		List<SessionDetailsDTO> sessions = sessionService
				.getCoreSessions(labs.stream().map(QueueSession::getSession).toList());
		return sessions.stream().filter(s -> s.getStart().isBefore(now) &&
				s.getEndTime().isAfter(now)).count();
	}

	/**
	 * Count the number of labs in a list for which slot selection is currently allowed.
	 *
	 * @param  labs The labs to count for.
	 * @return      The number of labs for which slots currently can be reserved.
	 */
	public long countLabsWithOpenSlotSelection(List<Lab> labs) {
		LocalDateTime now = LocalDateTime.now();
		return labs.stream().filter(l -> {
			switch (l.getType()) {
				case SLOTTED, EXAM -> {
					@SuppressWarnings("unchecked") AbstractSlottedLab<TimeSlot> lab = (AbstractSlottedLab<TimeSlot>) l;
					return lab.getSlottedLabConfig().getSelectionOpensAt().isBefore(now)
							&& sessionService.getCoreSession(lab.getSession()).getEndTime().isAfter(now);
				}
				default -> {
					return false;
				}
			}
		}).count();
	}

	/**
	 * Retrieves all the assignments an assistant is allowed to take. Mainly useful for shared labs where the
	 * TA is not a TA for all the courses.
	 *
	 * @param  lab       The lab for which to find the assignments.
	 * @param  assistant The TA for whom to find the assignments.
	 * @return           A list of assignments a TA is allowed to get.
	 */
	public List<AssignmentSummaryDTO> getAllowedAssignmentsInLab(Lab lab, Person assistant) {
		var sessionDetails = sessionService.getCoreSession(lab.getSession());
		var allowedAssignments = sessionDetails.getAssignments().stream().toList();
		if (sessionDetails.getEditionCollection() != null) {
			var collection = ecCache.getRequired(sessionDetails.getEditionCollection().getId());
			var roles = collection.getEditions().stream().map(e -> rService.rolesForPersonInEdition(e,
					assistant)).flatMap(List::stream).filter(rService::isStaff).toList();
			allowedAssignments = roles.stream()
					.map(r -> mApi.getModuleByEdition(r.getEdition().getId()).collectList().block())
					.filter(Objects::nonNull)
					.flatMap(Collection::stream).map(ModuleDetailsDTO::getAssignments)
					.flatMap(List::stream).toList();
		}
		return allowedAssignments;
	}

	/**
	 * Gets the online modes in a lab session.
	 *
	 * @param  labSessionId The ID of the lab session
	 * @return              The online modes associated to the lab.
	 */
	public Set<OnlineMode> getOnlineModesInLabSession(Long labSessionId) {
		if (labSessionId == null) {
			return emptySet();
		}
		var labs = lr.findAllBySessions(List.of(labSessionId));
		return labs.stream().map(Lab::getOnlineModes).flatMap(Set::stream).collect(Collectors.toSet());
	}

	/**
	 * Adds students, identified by their netid in a CSV file to an exam lab to give them priority during the
	 * review.
	 *
	 * @param  exam                The exam to which they should be added.
	 * @param  file                The csv file containing netid's
	 * @throws EmptyCsvException
	 * @throws InvalidCsvException
	 */
	@Transactional
	public void addStudentsToReview(ExamLab exam, MultipartFile file)
			throws EmptyCsvException, InvalidCsvException {
		List<PersonDetailsDTO> persons = CsvPerson.readCsv(file).stream()
				.map(s -> pca.getPersonByUsername(s.getUsername()).block()).toList();
		exam.setPickedStudents(persons.stream().map(PersonDetailsDTO::getId).collect(Collectors.toSet()));
	}

	@Transactional
	public Presentation getLabPresentation(Lab lab) {
		if (lab.getPresentation() == null) {
			Presentation presentation = new Presentation();
			presentation.setLab(lab);
			lab.setPresentation(presentation);
			pr.save(presentation);
		}
		return lab.getPresentation();
	}

	/**
	 * Syncs session objects between queue and core. This is a temporary fix before we find out a more
	 * permanent solution. TODO: Get rid of this (see above).
	 *
	 * @param qs The QueueSession in question.
	 */
	@Transactional
	public void syncQueueSessionWithCore(QueueSession<?> qs) {
		mCache.getAndHandle(qs.getModules().stream(), id -> qs.getModules().remove(id));

		if (qs instanceof Lab lab) {
			aCache.getAndHandle(
					lab.getAllowedRequests().stream().map(AllowedRequest::getAssignment).distinct(),
					id -> lab.getAllowedRequests().removeIf(ar -> Objects.equals(ar.getAssignment(), id)));
		}
	}

	public OptionalDouble currentWaitingTime(Lab lab) {
		if (lab instanceof RegularLab) {
			return currentWaitingTime((RegularLab) lab);
		} else if (lab instanceof AbstractSlottedLab<?>) {
			return currentWaitingTime((AbstractSlottedLab<? extends TimeSlot>) lab);
		}
		return OptionalDouble.empty();
	}

	public OptionalDouble currentWaitingTime(AbstractSlottedLab<? extends TimeSlot> lab) {
		var labEndTime = sessionService.getSessionDTOFromSession(lab).getEndTime();
		Stream<? extends TimeSlot> slots = lab.getTimeSlots().stream();
		OptionalDouble baseWaitTime;
		LocalDateTime effectiveEndTime;

		if (labEndTime.isBefore(now())) {
			effectiveEndTime = labEndTime;
			baseWaitTime = OptionalDouble.of(0);
		} else {
			slots = slots.filter(ts -> ts.getSlot().getOpensAt().isBefore(now()));
			effectiveEndTime = now();
			baseWaitTime = OptionalDouble.empty();
		}

		// Find the last started timeslot with pending requests, if any.
		// Otherwise, find the last started timeslot with requests.
		// Then pick the first created pending request, if any. Otherwise, pick the first created request.
		return slots.max(Comparator.<TimeSlot>comparingInt(ts -> ts.hasPendingRequests() ? 1 : -1)
				.thenComparingInt(ts -> ts.getRequests().size() > 0 ? 1 : -1)
				.thenComparing(ts -> ts.getSlot().getOpensAt()))
				.stream()
				.flatMap(ts -> ts.getRequests().stream())
				.max(Comparator
						.<LabRequest>comparingInt(r -> r.getEventInfo().getStatus().isPending() ? 1 : -1)
						.thenComparing(Comparator.nullsFirst(Comparator
								.<LabRequest, LocalDateTime>comparing(Request::getCreatedAt)
								.reversed())))
				.map(r -> r.sensibleWaitingTime(effectiveEndTime))
				.orElse(baseWaitTime);

	}

	public OptionalDouble currentWaitingTime(RegularLab lab) {
		var labEndTime = sessionService.getSessionDTOFromSession(lab).getEndTime();

		if (labEndTime.isBefore(LocalDateTime.now())) {
			// Lab is over, display waiting time of current requests or else 0
			return lab.getPendingRequests().stream()
					.min(Comparator.comparing(LabRequest::getCreatedAt))
					.map(r -> r.sensibleWaitingTime(labEndTime))
					.orElse(OptionalDouble.of(0));
		} else {
			// Lab is running. Get the waiting time of the oldest pending request.
			// If nothing is pending, report the waiting time of the last handled request.
			// If nothing was handled, report no waiting time.
			return lab.getPendingRequests().stream()
					.min(Comparator.comparing(LabRequest::getCreatedAt))
					.map(LabRequest::sensibleWaitingTime)
					.orElseGet(() -> lab.getHandled().stream()
							.max(Comparator.comparing(LabRequest::getCreatedAt))
							.map(LabRequest::sensibleWaitingTime)
							.orElse(OptionalDouble.empty()));
		}

	}

	/**
	 * Gets the average waiting time experienced by students in this lab over the last hour. If the lab is
	 * over, this returns the average waiting time over the entire lab instead.
	 *
	 * @return The average waiting time if one can be calculated, for fresh labs this is empty.
	 */
	public OptionalDouble averageWaitingTime(Lab lab) {
		var labEndTime = sessionService.getSessionDTOFromSession(lab).getEndTime();
		if (labEndTime.isBefore(now())) {
			// past session, give average over the session
			return lab.getHandled().stream()
					.flatMapToDouble(r -> r.sensibleWaitingTime(labEndTime).stream())
					.average();
		} else {
			return lab.getHandled().stream()
					.filter(r -> r.getEventInfo().getLastEventAt().isAfter(now().minusHours(1)))
					.flatMapToDouble(r -> r.sensibleWaitingTime().stream())
					.average();
		}
	}

	public boolean allowsRequest(QueueSession<?> session, Request<?> request) {
		if (!labRequestConstraintService.allowsRequest(session.getConstraints(), request)
				&& Objects.equals(request.getSession().getId(), session.getId())) {
			return false;
		}
		if (session instanceof Lab lab) {
			SessionDetailsDTO coreSession = sessionService.getCoreSession(lab.getSession());
			return request.getRoom() == null ? ((LabRequest) request).getOnlineMode() != null
					: coreSession.getRooms().stream()
							.anyMatch(room -> Objects.equals(room.getId(), request.getRoom()));
		}
		return true;
	}
}
