/*
 * 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.controller;

import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.*;
import static nl.tudelft.queue.service.LabService.SessionType.REGULAR;
import static nl.tudelft.queue.service.LabService.SessionType.SHARED;

import java.io.IOException;
import java.util.*;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.data.rest.webmvc.ResourceNotFoundException;
import org.springframework.http.*;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import com.google.common.collect.Maps;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import jakarta.transaction.Transactional;
import nl.tudelft.labracore.api.StudentGroupControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
import nl.tudelft.labracore.lib.security.user.Person;
import nl.tudelft.librador.dto.DTOConverter;
import nl.tudelft.librador.resolver.annotations.PathEntity;
import nl.tudelft.queue.cache.*;
import nl.tudelft.queue.csv.EmptyCsvException;
import nl.tudelft.queue.csv.InvalidCsvException;
import nl.tudelft.queue.dto.create.labs.CapacitySessionCreateDTO;
import nl.tudelft.queue.dto.create.labs.ExamLabCreateDTO;
import nl.tudelft.queue.dto.create.labs.RegularLabCreateDTO;
import nl.tudelft.queue.dto.create.labs.SlottedLabCreateDTO;
import nl.tudelft.queue.dto.create.requests.LabRequestCreateDTO;
import nl.tudelft.queue.dto.create.requests.SelectionRequestCreateDTO;
import nl.tudelft.queue.dto.patch.PresentationPatchDTO;
import nl.tudelft.queue.dto.patch.RequestPatchDTO;
import nl.tudelft.queue.dto.patch.SlottedLabTimeSlotCapacityPatchDTO;
import nl.tudelft.queue.dto.patch.labs.CapacitySessionPatchDTO;
import nl.tudelft.queue.dto.patch.labs.ExamLabPatchDTO;
import nl.tudelft.queue.dto.patch.labs.RegularLabPatchDTO;
import nl.tudelft.queue.dto.patch.labs.SlottedLabPatchDTO;
import nl.tudelft.queue.dto.util.DistributeRequestsDTO;
import nl.tudelft.queue.dto.util.RequestTableFilterDTO;
import nl.tudelft.queue.dto.view.RequestViewDTO;
import nl.tudelft.queue.model.QueueSession;
import nl.tudelft.queue.model.Request;
import nl.tudelft.queue.model.embeddables.AllowedRequest;
import nl.tudelft.queue.model.enums.Language;
import nl.tudelft.queue.model.enums.QueueSessionType;
import nl.tudelft.queue.model.enums.RequestType;
import nl.tudelft.queue.model.enums.SelectionProcedure;
import nl.tudelft.queue.model.labs.*;
import nl.tudelft.queue.model.misc.CustomSlide;
import nl.tudelft.queue.repository.CustomSlideRepository;
import nl.tudelft.queue.repository.LabRequestRepository;
import nl.tudelft.queue.repository.PresentationRepository;
import nl.tudelft.queue.repository.QueueSessionRepository;
import nl.tudelft.queue.service.*;
import nl.tudelft.queue.service.dto.PresentationViewDTOService;
import nl.tudelft.queue.service.dto.QueueSessionViewDTOService;
import nl.tudelft.queue.service.dto.RequestViewDTOService;

@Controller
public class LabController {
	@Autowired
	private QueueSessionRepository qsRepository;

	@Autowired
	private PresentationRepository pr;

	@Autowired
	private CustomSlideRepository csr;

	@Autowired
	private LabRequestRepository lrr;

	@Autowired
	private LabService ls;

	@Autowired
	private EditionService es;

	@Autowired
	private RequestTableService rts;

	@Autowired
	private PermissionService ps;

	@Autowired
	private RequestService rs;

	@Autowired
	private AssignmentCacheManager aCache;

	@Autowired
	private CohortCacheManager cCache;

	@Autowired
	private EditionCacheManager eCache;

	@Autowired
	private EditionCollectionCacheManager ecCache;

	@Autowired
	private EditionRolesCacheManager erCache;

	@Autowired
	private ModuleCacheManager mCache;

	@Autowired
	private SessionCacheManager sCache;

	@Autowired
	private StudentGroupCacheManager sgCache;

	@Autowired
	private RoomCacheManager rCache;

	@Autowired
	private RoleDTOService roleService;

	@Autowired
	private RequestService requestService;

	@Autowired
	private StudentGroupControllerApi sgApi;

	@Autowired
	private HttpSession session;

	@Autowired
	private ModelMapper mapper;

	@Autowired
	private SessionService sessionService;

	@Autowired
	private PresentationViewDTOService presentationViewDTOService;

	@Autowired
	private QueueSessionViewDTOService queueSessionViewDTOService;

	@Autowired
	private RequestViewDTOService requestViewDTOService;

	@Autowired
	private DTOConverter converter;

	/**
	 * Gets the page with information on the lab with the given id. This page is different for TAs than for
	 * students. For students, this page shows their history of requests and their current requests. For TAs,
	 * this page mostly shows the full history of all requests that have previously happened in a lab. For
	 * finished labs, this is the main info available on the lab page, together with some general information
	 * on the lab.
	 *
	 * @param  qSession The lab to display on the page.
	 * @param  person   The currently authenticated person.
	 * @param  model    The model to fill out for Thymeleaf template resolution.
	 * @return          The Thymeleaf template to resolve.
	 */
	@GetMapping("/lab/{qSession}")
	@PreAuthorize("@permissionService.canViewSession(#qSession)")
	public <R extends Request<?>> String getSessionView(@PathEntity QueueSession<R> qSession,
			@AuthenticatedPerson Person person,
			Model model) {
		setEnqueuePageAttributes(qSession, model, person);

		RequestTableFilterDTO filter = rts.checkAndStoreFilterDTO(null, "/lab/" + qSession.getId());
		model.addAttribute("filter", filter);

		// Either get all requests for the lab if the person is an assistant or just those specific to the person.
		List<? extends Request<?>> requests;
		if (qSession instanceof Lab lab) {
			requests = lrr.findAllByFilter(List.of(lab), filter, Language.ANY);
		} else {
			requests = qSession.getRequests();
		}
		//if the person should see only their own requests, they see their reqiests and their group requests
		if (!ps.canViewSessionRequests(qSession.getId())) {
			Optional<R> openRequestForGroup = rs.getOpenRequestForGroupOfPerson(person.getId(), qSession);
			requests = requests.stream()
					.filter(r -> (Objects.equals(r.getRequester(), person.getId()) || openRequestForGroup
							.map(groupRequest -> Objects.equals(groupRequest, r)).orElse(false)))
					.toList();
		}
		model.addAttribute("requests",
				rts.convertRequestsToView(requests)
						.stream()
						.sorted(Comparator.comparing((RequestViewDTO<?> r) -> r.getCreatedAt()).reversed())
						.collect(Collectors.toList()));

		// Check whether there is an open request for the authenticated user and add it to the model
		Optional<R> currOptRequest = qSession.getOpenRequestForPerson(person.getId())
				.or(() -> rs.getOpenRequestForGroupOfPerson(person.getId(), qSession));

		if (currOptRequest.isPresent()) {
			Request<?> currRequest = currOptRequest.get();
			// If the user is currently being processed it should be looking at the request page
			if (currRequest.getEventInfo().getStatus().isProcessing()) {
				return "redirect:/request/" + currRequest.getId();
			}
			model.addAttribute("current", requestViewDTOService.convert(currRequest));
			model.addAttribute("currentDto",
					converter.getModelMapper().map(currRequest, RequestPatchDTO.class));
		}

		// Check whether there is a finished selection request for the authenticated user and add it
		if (qSession instanceof CapacitySession) {
			((CapacitySession) qSession).getProcessedSelectionRequest(person.getId())
					.ifPresent(r -> model.addAttribute("selectionResult", requestViewDTOService.convert(r)));
		}

		// Filter data
		List<ModuleDetailsDTO> modules = mCache.getAndIgnoreMissing(qSession.getModules().stream());
		Map<Long, CourseSummaryDTO> assignmentsWithCourses = modules.stream()
				.flatMap(m -> m.getAssignments().stream()
						.map(a -> Maps.immutableEntry(a.getId(),
								eCache.getRequired(m.getEdition().getId()).getCourse())))
				.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

		List<AssignmentSummaryDTO> allAssignments = modules.stream().flatMap(m -> m.getAssignments().stream())
				.sorted(Comparator.comparing(a -> (assignmentsWithCourses.containsKey(a.getId())
						? assignmentsWithCourses.get(a.getId())
						: "") + a.getName()))
				.toList();

		SessionDetailsDTO session = sessionService.getCoreSession(qSession.getSession());

		Map<Long, String> allAssignmentsMap = obtainAssignmentsMap(allAssignments, session);

		setMultipleRolesAttributes(qSession, model, person, modules);

		model.addAttribute("modules", modules);
		model.addAttribute("allAssignments", allAssignments);
		model.addAttribute("allAssignmentsMap", allAssignmentsMap);
		model.addAttribute("courses",
				eCache.getAndIgnoreMissing(modules.stream().map(m -> m.getEdition().getId())).stream()
						.map(EditionDetailsDTO::getCourse).distinct().toList());
		model.addAttribute("assignmentsWithCourses",
				assignmentsWithCourses);
		model.addAttribute("assistants", erCache
				.getAndIgnoreMissing(sessionService.getSessionDTOFromSession(qSession)
						.getEditions().stream().map(EditionSummaryDTO::getId))
				.stream()
				.flatMap(e -> e.getRoles().stream())
				.filter(r -> Set.of(TEACHER, TEACHER_RO, HEAD_TA, TA).contains(r.getType()))
				.map(RolePersonDetailsDTO::getPerson)
				.sorted(Comparator.comparing(PersonSummaryDTO::getDisplayName))
				.distinct()
				.toList());

		return "lab/view/" + qSession.getType().name().toLowerCase();
	}

	/**
	 * Applies any changes to the capacities of time slots in the given lab using the Patch DTO in the body of
	 * the request.
	 *
	 * @param  lab The lab for which to change the capacities of time slots.
	 * @param  dto The DTO indicating what to change.
	 * @return     A redirect to the lab overview page.
	 */
	@Transactional
	@PostMapping("/lab/{lab}/capacities")
	@PreAuthorize("@permissionService.canManageSession(#lab)")
	public String editLabCapacities(@PathEntity Lab lab,
			SlottedLabTimeSlotCapacityPatchDTO dto) {
		if (lab instanceof AbstractSlottedLab<?>) {
			dto.apply((AbstractSlottedLab<?>) lab, converter);
		}

		return "redirect:/lab/" + lab.getId();
	}

	/**
	 *
	 * @param  lab    The lab that we will distribute requests for
	 * @param  person The distributor of the requests
	 * @param  dto    The DTO containing the assignments and assistants to distribute
	 * @return        A redirect to the lab overview page.
	 */
	@Transactional
	@PostMapping("/lab/{lab}/distribute")
	@PreAuthorize("@permissionService.canManageSession(#lab)")
	public String distributeRequests(@PathEntity Lab lab, @AuthenticatedPerson Person person,
			DistributeRequestsDTO dto) {

		dto.getEditionSelections()
				.forEach(edition -> requestService.distributeRequests(edition.getSelectedAssignments(),
						edition.getSelectedAssistants(), person, lab));

		return "redirect:/lab/" + lab.getId();
	}

	/**
	 * Gets the student enqueue view. This page displays the form that needs to be filled out by the student
	 * to successfully enrol into the given session.
	 *
	 * @param  qSession The session the student is enrolling in.
	 * @param  model    The model to fill out for Thymeleaf template resolution.
	 * @return          The Thymeleaf template to resolve.
	 */
	@GetMapping("/lab/{qSession}/enqueue")
	@PreAuthorize("@permissionService.canEnqueueSelfNow(#qSession.id)")
	public String getEnqueueView(@PathEntity QueueSession<?> qSession,
			@AuthenticatedPerson Person person,
			Model model) {
		setEnqueuePageAttributes(qSession, model, person);

		if (qSession instanceof Lab) {
			model.addAttribute("request", new LabRequestCreateDTO());
			model.addAttribute("rType", "lab");

			if (session.getAttribute("lastKnownLocation") != null) {
				model.addAttribute("lastKnownLocation", session.getAttribute("lastKnownLocation"));
			}

			return "lab/enqueue/lab";
		} else {
			model.addAttribute("request", new SelectionRequestCreateDTO());
			model.addAttribute("rType", "capacity");

			return "lab/enqueue/capacity";
		}
	}

	/**
	 * Gets the student Q&A view.
	 *
	 * @param  lab   The lab the student is enrolling in.
	 * @param  model The model to fill out for Thymeleaf template resolution.
	 * @return       The Thymeleaf template to resolve.
	 */
	@GetMapping("/lab/{lab}/question/{assignment}")
	@PreAuthorize("@permissionService.canEnqueueSelfNow(#lab.id)")
	public String getQuestionView(@PathEntity Lab lab,
			@PathVariable Long assignment,
			@AuthenticatedPerson Person person,
			Model model) {
		setEnqueuePageAttributes(lab, model, person);

		model.addAttribute("assignment", aCache.getRequired(assignment));

		return "lab/question";
	}

	/**
	 * Gets the view of the enqueue page after no answers were helpful.
	 *
	 * @param  lab   The lab the student is enrolling in
	 * @param  model The model to fill out for Thymeleaf template resolution
	 * @return       The Thymeleaf template to resolve
	 */
	@GetMapping("/lab/{lab}/enqueue/question/{assignment}")
	@PreAuthorize("@permissionService.canEnqueueSelfNow(#lab.id)")
	public String getEnqueueAfterQuestionView(@PathEntity Lab lab,
			@PathVariable Long assignment,
			@RequestParam String question,
			@AuthenticatedPerson Person person,
			Model model) {
		setEnqueuePageAttributes(lab, model, person);

		model.addAttribute("assignment", aCache.getRequired(assignment));
		model.addAttribute("question", question);
		model.addAttribute("request", new LabRequestCreateDTO());
		model.addAttribute("rType", "lab");

		return "lab/enqueue/question";
	}

	/**
	 * Performs the enqueue action. This action allows a student to create a new request in a session they are
	 * participating in.
	 *
	 * @param  student The student making the request.
	 * @param  lab     The session in which the request is to be created.
	 * @param  dto     The dto representing the values the student has entered for creating their request.
	 * @return         The Thymeleaf template to resolve or a redirect back to the lab page.
	 */
	@PostMapping("/lab/{lab}/enqueue/lab")
	@PreAuthorize("@permissionService.canEnqueueSelfNow(#lab.id)")
	public String enqueueInLab(@AuthenticatedPerson Person student,
			@PathEntity Lab lab,
			@RequestParam(required = false, defaultValue = "false") Boolean doEnqueueExperimental,
			LabRequestCreateDTO dto) {
		if (lab.getEnableExperimental() && dto.getRequestType() == RequestType.QUESTION
				&& !doEnqueueExperimental) {
			return "redirect:/lab/" + lab.getId() + "/question/" + dto.getAssignment();
		}

		dto.setSession(lab);
		rs.createRequest(dto, student.getId(), true);

		if (dto.getComment() != null) {
			session.setAttribute("lastKnownLocation", dto.getComment());
		}

		return "redirect:/lab/" + lab.getId();
	}

	/**
	 * Performs the enqueue action. This action allows a student to create a new request in a session they are
	 * participating in.
	 *
	 * @param  student  The student making the request.
	 * @param  qSession The session in which the request is to be created.
	 * @param  dto      The dto representing the values the student has entered for creating their request.
	 * @return          The Thymeleaf template to resolve or a redirect back to the lab page.
	 */
	@PostMapping("/lab/{qSession}/enqueue/capacity")
	@PreAuthorize("@permissionService.canEnqueueSelfNow(#qSession.id)")
	public String enqueueInSession(@AuthenticatedPerson Person student,
			@PathEntity CapacitySession qSession,
			SelectionRequestCreateDTO dto) {
		dto.setSession(qSession);
		rs.createRequest(dto, student.getId(), true);

		return "redirect:/lab/" + qSession.getId();
	}

	/**
	 * Handles a post request to the close enqueue page. This simply handles a button press to close or open
	 * enqueueing in a session manually.
	 *
	 * @param  qSession     The session that is to be closed or opened manually.
	 * @param  closeEnqueue Whether to open or to close.
	 * @return              A redirect to the lab overview page.
	 */
	@Transactional
	@PostMapping("/lab/{qSession}/close-enqueue/{closeEnqueue}")
	@PreAuthorize("@permissionService.canManageSession(#qSession)")
	public String closeEnqueueInSession(@PathEntity QueueSession<?> qSession,
			@PathVariable Boolean closeEnqueue) {
		qSession.setEnqueueClosed(closeEnqueue);

		return "redirect:/lab/" + qSession.getId();
	}

	/**
	 * Gets the lab creation page. This page should be viewable by all teachers and managers (those that are
	 * allowed to create labs). This page allows for editing a lab before final creation of that lab.
	 *
	 * @param  editionId The id of the edition to create the lab in.
	 * @param  model     The model to fill out for Thymeleaf template resolution.
	 * @return           The Thymeleaf template to resolve.
	 */
	@GetMapping("/edition/{editionId}/lab/create")
	@PreAuthorize("@permissionService.canManageSessions(#editionId)")
	public String getLabCreateView(@PathVariable Long editionId,
			@RequestParam QueueSessionType type, Model model) {
		model.addAttribute("dto", type.newCreateDto());
		model.addAttribute("edition", eCache.getRequired(editionId));
		model.addAttribute("lType", type);

		setSessionEditingPageAttributes(List.of(editionId), model);

		return "lab/create/" + type.name().toLowerCase();
	}

	/**
	 * Gets the lab creation page. This page should be viewable by all teachers and managers (those that are
	 * allowed to create labs). This page allows for editing a lab before final creation of that lab.
	 *
	 * @param  editionCollectionId The id of the edition collection to create lab in.
	 * @param  model               The model to fill out for Thymeleaf template resolution.
	 * @return                     The Thymeleaf template to resolve.
	 */
	@GetMapping("/shared-edition/{editionCollectionId}/lab/create")
	@PreAuthorize("@permissionService.canManageSharedSessions(#editionCollectionId)")
	public String getSharedLabCreateView(@PathVariable Long editionCollectionId,
			@RequestParam QueueSessionType type, Model model) {
		var editionCollection = ecCache.getRequired(editionCollectionId);

		model.addAttribute("dto", type.newCreateDto());
		model.addAttribute("ec", editionCollection);
		model.addAttribute("lType", type);

		setSessionEditingPageAttributes(editionCollection.getEditions().stream()
				.map(EditionSummaryDTO::getId).collect(Collectors.toList()), model);

		return "lab/create/" + type.name().toLowerCase();
	}

	/**
	 * Creates a new capacity session using the session create DTO provided through the body of the POST
	 * request. Creates a session of the capacity type.
	 *
	 * @param  editionId The id of the edition this session is part of.
	 * @param  dto       The dto to apply to create a new session.
	 * @return           A redirect to the created session page.
	 */
	@PostMapping("/edition/{editionId}/lab/create/capacity")
	@PreAuthorize("@permissionService.canManageSessions(#editionId)")
	public String createCapacityLab(@PathVariable Long editionId,
			CapacitySessionCreateDTO dto) {
		ls.createSessions(dto, editionId, REGULAR);
		return "redirect:/edition/" + editionId + "/labs";
	}

	/**
	 * Creates a new lab using the lab create DTO provided through the body of the POST request. Creates a lab
	 * of the regular type.
	 *
	 * @param  editionId The id of the edition this lab is part of.
	 * @param  dto       The dto to apply to create a new lab.
	 * @return           A redirect to the created lab page.
	 */
	@PostMapping("/edition/{editionId}/lab/create/regular")
	@PreAuthorize("@permissionService.canManageSessions(#editionId)")
	public String createRegularLab(@PathVariable Long editionId,
			RegularLabCreateDTO dto) {
		ls.createSessions(dto, editionId, REGULAR);
		return "redirect:/edition/" + editionId + "/labs";
	}

	/**
	 * Creates a new lab using the lab create DTO provided through the body of the POST request. Creates a lab
	 * of the slotted type.
	 *
	 * @param  editionId The id of the edition this lab is part of.
	 * @param  dto       The dto to apply to create a new lab.
	 * @return           A redirect to the created lab page.
	 */
	@PostMapping("/edition/{editionId}/lab/create/slotted")
	@PreAuthorize("@permissionService.canManageSessions(#editionId)")
	public String createSlottedLab(@PathVariable Long editionId,
			SlottedLabCreateDTO dto) {
		ls.createSessions(dto, editionId, REGULAR);
		return "redirect:/edition/" + editionId + "/labs";
	}

	/**
	 * Creates a new lab using the lab create DTO provided through the body of the POST request. Creates a lab
	 * of the exam type.
	 *
	 * @param  editionId The id of the edition this lab is part of.
	 * @param  dto       The dto to apply to create a new lab.
	 * @return           A redirect to the created lab page.
	 */
	@PostMapping("/edition/{editionId}/lab/create/exam")
	@PreAuthorize("@permissionService.canManageSessions(#editionId)")
	public String createExamLab(@PathVariable Long editionId,
			ExamLabCreateDTO dto) {
		ls.createSessions(dto, editionId, REGULAR);
		return "redirect:/edition/" + editionId + "/labs";
	}

	/**
	 * Creates a new lab using the lab create DTO provided through the body of the POST request. This method
	 * creates one specifically for a shared edition. Creates a lab of the regular type.
	 *
	 * @param  editionCollectionId The id of the edition collection the lab will be part of.
	 * @param  dto                 The dto to apply to create a new lab.
	 * @return                     A redirect to the created lab page.
	 */
	@PostMapping("/shared-edition/{editionCollectionId}/lab/create/regular")
	@PreAuthorize("@permissionService.canManageSharedSessions(#editionCollectionId)")
	public String createRegularSharedLab(@PathVariable Long editionCollectionId,
			RegularLabCreateDTO dto) {
		ls.createSessions(dto, editionCollectionId, SHARED);
		return "redirect:/shared-edition/" + editionCollectionId;
	}

	/**
	 * Creates a new lab using the lab create DTO provided through the body of the POST request. This method
	 * creates one specifically for a shared edition. Creates a lab of the slotted type.
	 *
	 * @param  editionCollectionId The id of the edition collection the lab will be part of.
	 * @param  dto                 The dto to apply to create a new lab.
	 * @return                     A redirect to the created lab page.
	 */
	@PostMapping("/shared-edition/{editionCollectionId}/lab/create/slotted")
	@PreAuthorize("@permissionService.canManageSharedSessions(#editionCollectionId)")
	public String createSlottedSharedLab(@PathVariable Long editionCollectionId,
			SlottedLabCreateDTO dto) {
		ls.createSessions(dto, editionCollectionId, SHARED);
		return "redirect:/shared-edition/" + editionCollectionId;
	}

	/**
	 * Creates a new lab using the lab create DTO provided through the body of the POST request. This method
	 * creates one specifically for a shared edition. Creates a lab of the exam type.
	 *
	 * @param  editionCollectionId The id of the edition collection the lab will be part of.
	 * @param  dto                 The dto to apply to create a new lab.
	 * @return                     A redirect to the created lab page.
	 */
	@PostMapping("/shared-edition/{editionCollectionId}/lab/create/exam")
	@PreAuthorize("@permissionService.canManageSharedSessions(#editionCollectionId)")
	public String createExamSharedLab(@PathVariable Long editionCollectionId,
			ExamLabCreateDTO dto) {
		ls.createSessions(dto, editionCollectionId, SHARED);
		return "redirect:/shared-edition/" + editionCollectionId;
	}

	/**
	 * Handles a GET request to the session delete endpoint by soft-deleting the session.
	 *
	 * @param  qSession The session to delete.
	 * @return          A redirect back to the edition lab overview.
	 */
	@PostMapping("/lab/{qSession}/remove")
	@PreAuthorize("@permissionService.canManageSession(#qSession)")
	public String deleteSession(@PathEntity QueueSession<?> qSession) {
		SessionDetailsDTO session = sessionService.getCoreSession(qSession.getSession());
		qsRepository.delete(qSession);

		if (session.getEditionCollection() != null) {
			return "redirect:/shared-edition/" + session.getEditionCollection().getId();
		}
		return "redirect:/edition/" + session.getEdition().getId() + "/labs";
	}

	/**
	 * Gets the lab creation page copied from the lab targeted in the url of this request.
	 *
	 * @param  qSession The lab that is targeted for copying.
	 * @param  model    The model to fill out for Thymeleaf template resolution.
	 * @return          The Thymeleaf template to resolve.
	 */
	// TODO: Check whether this permission cannot be better: currently one can copy a lab if they can manage that lab,
	//        but these permissions should be the same as creating a new lab.
	@GetMapping("/lab/{qSession}/copy")
	@PreAuthorize("@permissionService.canManageSession(#qSession)")
	public String getLabCopyView(@PathEntity QueueSession<?> qSession,
			Model model) {

		model.addAttribute("dto", sessionService.copyLabCreateDTO(qSession));
		model.addAttribute("lType", qSession.getType());

		setSessionEditingPageAttributes(qSession, model);

		return "lab/create/" + qSession.getType().name().toLowerCase();
	}

	/**
	 * Gets the lab editing page.
	 *
	 * @param  qSession The lab that is to be edited by this page.
	 * @param  model    The model to fill out for Thymeleaf template resolution.
	 * @return          The Thymeleaf template to resolve.
	 */
	@GetMapping("/lab/{qSession}/edit")
	@PreAuthorize("@permissionService.canManageSession(#qSession)")
	public String getLabUpdateView(@PathEntity QueueSession<?> qSession, Model model) {
		model.addAttribute("dto", qSession.newPatchDTO());
		model.addAttribute("lab", qSession);

		setSessionEditingPageAttributes(qSession, model);

		return "lab/edit/" + qSession.getType().name().toLowerCase();
	}

	/**
	 * Updates the given lab entity using the given lab patch DTO. The lab in question is of the regular type.
	 *
	 * @param  lab The lab to update.
	 * @param  dto The dto representing the changes to apply to the lab.
	 * @return     A redirect to the lab view page.
	 */
	@Transactional
	@PostMapping("/lab/{lab}/edit/regular")
	@PreAuthorize("@permissionService.canManageSession(#lab)")
	public String updateSession(@PathEntity RegularLab lab, RegularLabPatchDTO dto) {
		ls.updateSession(dto, lab);
		return "redirect:/lab/" + lab.getId();
	}

	/**
	 * Updates the given lab entity using the given lab patch DTO. The lab in question is of the slotted type.
	 *
	 * @param  lab The lab to update.
	 * @param  dto The dto representing the changes to apply to the lab.
	 * @return     A redirect to the lab view page.
	 */
	@Transactional
	@PostMapping("/lab/{lab}/edit/slotted")
	@PreAuthorize("@permissionService.canManageSession(#lab)")
	public String updateSession(@PathEntity SlottedLab lab, SlottedLabPatchDTO dto) {
		ls.updateSession(dto, lab);
		return "redirect:/lab/" + lab.getId();
	}

	/**
	 * Updates the given lab entity using the given lab patch DTO. The lab in question is of the exam type.
	 *
	 * @param  lab The lab to update.
	 * @param  dto The dto representing the changes to apply to the lab.
	 * @return     A redirect to the lab view page.
	 */
	@Transactional
	@PostMapping("/lab/{lab}/edit/exam")
	@PreAuthorize("@permissionService.canManageSession(#lab)")
	public String updateSession(@PathEntity ExamLab lab, ExamLabPatchDTO dto) {
		ls.updateSession(dto, lab);
		return "redirect:/lab/" + lab.getId();
	}

	/**
	 * Updates the given session entity using the given session patch DTO. The session in question is of the
	 * limited capacity type.
	 *
	 * @param  session The session to update.
	 * @param  dto     The dto representing the changes to apply to the session.
	 * @return         A redirect to the session view page.
	 */
	@Transactional
	@PostMapping("/lab/{session}/edit/capacity")
	@PreAuthorize("@permissionService.canManageSession(#session)")
	public String updateSession(@PathEntity CapacitySession session, CapacitySessionPatchDTO dto) {
		SelectionProcedure oldProcedure = session.getCapacitySessionConfig().getProcedure();
		ls.updateSession(dto, session);
		ls.updateCapacitySessionRequests(session, oldProcedure);
		return "redirect:/lab/" + session.getId();
	}

	/**
	 * Exports the entire session request info to a single CSV file.
	 *
	 * @param  session     The session to export to a file.
	 * @return             The Resource representing the file to download in a response with appropriate
	 *                     headers set.
	 * @throws IOException when something goes wrong when writing the file.
	 */
	@GetMapping("/lab/{session}/export")
	@PreAuthorize("@permissionService.canManageSession(#session)")
	public ResponseEntity<Resource> exportSession(@PathEntity QueueSession<?> session) throws IOException {
		var resource = ls.sessionToCsv(session);

		return ResponseEntity.ok()
				.contentType(MediaType.APPLICATION_OCTET_STREAM)
				.contentLength(resource.contentLength())
				.header(HttpHeaders.CONTENT_DISPOSITION,
						ContentDisposition.attachment()
								.filename("session-export-" + session.getId() + ".csv")
								.build().toString())
				.body(resource);
	}

	/**
	 * Imports a list of students which should get priority for reviewing during an exam lab.
	 *
	 * @param  session             The exam lab to which they should be added.
	 * @param  csv                 The csv file containing NetIDs for these students.
	 * @return
	 * @throws EmptyCsvException
	 * @throws InvalidCsvException
	 */
	@PostMapping("/lab/{session}/import")
	@PreAuthorize("@permissionService.canManageSession(#session)")
	public String importStudents(@PathEntity ExamLab session,
			@RequestParam("file") MultipartFile csv) throws EmptyCsvException, InvalidCsvException {
		ls.addStudentsToReview(session, csv);
		return "redirect:/lab/" + session.getId();
	}

	/**
	 * Sets the model attributes for an enqueueing or editing requests page.
	 *
	 * @param qSession The session which the request takes place in.
	 * @param model    The model to fill using edition and lab information.
	 * @param person   The person used to fill in the information.
	 */
	@Transactional
	public void setEnqueuePageAttributes(QueueSession<?> qSession, Model model, Person person) {
		SessionDetailsDTO session = sessionService.getCoreSession(qSession.getSession());
		ls.syncQueueSessionWithCore(qSession);
		List<RoomDetailsDTO> rooms = session.getRooms().stream()
				.sorted(Comparator.comparing(RoomDetailsDTO::getName)).toList();

		model.addAttribute("qSession", queueSessionViewDTOService.convert(qSession));
		ls.setOrganizationInModel(session, model);

		model.addAttribute("buildings", rooms.stream().map(RoomDetailsDTO::getBuilding)
				.sorted(Comparator.comparing(BuildingSummaryDTO::getName)).distinct().toList());
		model.addAttribute("rooms",
				rooms.stream().sorted(Comparator.comparing(RoomDetailsDTO::getName)).toList());

		if (qSession instanceof Lab) {
			var lab = (Lab) qSession;
			var isStaffInAll = session.getEditions().stream().allMatch(e -> roleService
					.rolesForPersonInEdition(e, person).stream().allMatch(roleService::isStaff));
			List<AssignmentDetailsDTO> allowedAssignments = aCache
					.getAndHandleAll(lab.getAllowedRequests().stream()
							.map(AllowedRequest::getAssignment).distinct().collect(Collectors.toList()),
							assignmentIds -> lab.getAllowedRequests()
									.removeIf(a -> assignmentIds.contains(a.getAssignment())))
					.stream().filter(a -> {
						var edition = mCache.getRequired(a.getModule().getId()).getEdition();
						return isStaffInAll || roleService.rolesForPersonInEdition(edition, person).stream()
								.noneMatch(roleService::isStaff);
					}).toList();
			Map<Long, CourseSummaryDTO> editionsToCourses = session.getEditions().stream()
					.collect(Collectors.toMap(EditionSummaryDTO::getId,
							edition -> eCache.getRequired(edition.getId()).getCourse()));
			Map<Long, String> assignments = allowedAssignments.stream()
					.collect(Collectors.toMap(AssignmentDetailsDTO::getId, a -> {
						var edition = mCache.getRequired(a.getModule().getId()).getEdition();
						return session.getEditions().size() == 1
								? a.getName()
								: eCache.getRequired(edition.getId()).getCourse().getName() + " - "
										+ a.getName();
					}));

			Map<Long, Long> notEnqueueAble = allowedAssignments.stream().filter(a -> {
				var edition = mCache.getRequired(a.getModule().getId()).getEdition();
				return roleService.rolesForPersonInEdition(edition, person).isEmpty();
			}).collect(Collectors.toMap(AssignmentDetailsDTO::getId, a -> {
				var edition = mCache.getRequired(a.getModule().getId()).getEdition();
				return edition.getId();
			}));

			if (qSession instanceof AbstractSlottedLab<?>) {
				model.addAttribute("distributeRequestsDto",
						new DistributeRequestsDTO(editionsToCourses.size()));
			}

			Set<Long> alreadyInGroup = sgCache.getByPerson(person.getId()).stream()
					.map(g -> g.getModule().getId()).collect(Collectors.toSet());
			Set<Long> hasEmptyGroups = qSession.getModules().stream()
					.filter(m -> !alreadyInGroup.contains(m))
					.filter(m -> sgApi.getAllGroupsInModule(m).any(g -> g.getMemberUsernames().isEmpty())
							.block())
					.collect(Collectors.toSet());

			model.addAttribute("needToJoinGroup",
					allowedAssignments.stream().filter(a -> hasEmptyGroups.contains(a.getModule().getId()))
							.map(AssignmentDetailsDTO::getId).collect(Collectors.toSet()));
			model.addAttribute("assignments", assignments);
			model.addAttribute("editionsToCourses", editionsToCourses);
			model.addAttribute("notEnqueueAble", notEnqueueAble);
			model.addAttribute("types", lab.getAllowedRequests().stream()
					.collect(Collectors.groupingBy(AllowedRequest::getAssignment,
							Collectors.mapping(ar -> ar.getType().displayName(), Collectors.toSet()))));
		} else {
			model.addAttribute("modules",
					mCache.getAndHandle(qSession.getModules().stream(),
							moduleID -> qSession.getModules().remove(moduleID))
							.stream().sorted(Comparator.comparing(ModuleDetailsDTO::getName))
							.collect(Collectors.toList()));
		}
	}

	/**
	 * Sets the model attributes for a lab editing or creation page.
	 *
	 * @param editionIds The ids of the editions the lab will be in.
	 * @param model      The model to fill using edition information.
	 */
	private void setSessionEditingPageAttributes(List<Long> editionIds, Model model) {
		var editions = eCache.getAndIgnoreMissing(editionIds);

		var modules = mCache.getAndIgnoreMissing(editions.stream()
				.flatMap(e -> e.getModules().stream().map(ModuleSummaryDTO::getId).distinct()));
		var assignments = aCache.getAndIgnoreMissing(modules.stream()
				.flatMap(m -> m.getAssignments().stream().map(AssignmentSummaryDTO::getId)).distinct());
		var clusters = cCache
				.getAndIgnoreMissing(editions.stream().map(e -> e.getCohort().getId()).distinct())
				.stream().flatMap(cohort -> cohort.getClusters().stream()).distinct()
				.sorted(Comparator.comparing(ClusterSummaryDTO::getName)).collect(Collectors.toList());

		var divisions = modules.stream()
				.flatMap(m -> m.getDivisions().stream()).collect(Collectors.toList());
		var rooms = rCache.getAll();

		modules.sort(Comparator.comparing(ModuleDetailsDTO::getName));
		assignments.sort(Comparator.comparing(AssignmentDetailsDTO::getName));
		rooms.sort(Comparator.comparing((RoomDetailsDTO r) -> r.getBuilding().getName() + r.getName()));

		model.addAttribute("editions", editions);
		model.addAttribute("editionMap",
				editions.stream().collect(Collectors.toMap(EditionDetailsDTO::getId, Function.identity())));
		model.addAttribute("modules", modules);
		model.addAttribute("moduleMap",
				modules.stream().collect(Collectors.toMap(ModuleDetailsDTO::getId, Function.identity())));
		model.addAttribute("assignments", assignments);

		model.addAttribute("clusters", clusters);
		model.addAttribute("divisions", divisions);

		model.addAttribute("rooms", rooms);
		model.addAttribute("buildings", rooms.stream()
				.map(RoomDetailsDTO::getBuilding).distinct()
				.sorted(Comparator.comparing(BuildingSummaryDTO::getName))
				.collect(Collectors.toList()));
	}

	private Map<Long, String> obtainAssignmentsMap(List<AssignmentSummaryDTO> assignments,
			SessionDetailsDTO session) {
		return assignments.stream()
				.collect(Collectors.toMap(AssignmentSummaryDTO::getId, a -> {
					var module = aCache.getRequired(a.getId()).getModule();
					var edition = mCache.getRequired(module.getId()).getEdition();
					return session.getEditions().size() == 1
							? a.getName()
							: eCache.getRequired(edition.getId()).getCourse().getName() + " - "
									+ a.getName();
				}));
	}

	/**
	 * Sets the attributes necessary for the page for editing a lab.
	 *
	 * @param qSession The (existing) lab that is to be edited.
	 * @param model    The model to fill using lab information.
	 */
	private void setSessionEditingPageAttributes(QueueSession<?> qSession, Model model) {
		SessionDetailsDTO session = sessionService.getCoreSession(qSession.getSession());
		model.addAttribute("lSession", session);

		if (session.getEdition() != null) {
			model.addAttribute("edition", eCache.getRequired(session.getEdition().getId()));
		} else {
			model.addAttribute("ec", ecCache.getRequired(session.getEditionCollection().getId()));
		}

		setSessionEditingPageAttributes(session.getEditions().stream()
				.map(EditionSummaryDTO::getId).collect(Collectors.toList()), model);
	}

	/**
	 * Adds attributes to the model for instances when a person has multiple roles in a lab. Used to
	 * differentiate the assignments for which a student can enqueue from those for which the student can take
	 * requests.
	 *
	 * @param qSession the lab session for which to display the allowed assignments
	 * @param model    the model used to display the allowed assignments
	 * @param person   the person for which to find the allowed assignments
	 * @param modules  the modules contained by the lab session
	 */
	private void setMultipleRolesAttributes(QueueSession<?> qSession, Model model, Person person,
			List<ModuleDetailsDTO> modules) {
		List<RolePersonDetailsDTO> allRoles = erCache
				.getAndIgnoreMissing(sessionService.getSessionDTOFromSession(qSession)
						.getEditions().stream().map(EditionSummaryDTO::getId))
				.stream()
				.flatMap(e -> e.getRoles().stream()).toList();

		Map<Object, Object> allRolesForPersonPerEdition = allRoles.stream()
				.filter(r -> r.getPerson().getId().equals(person.getId()))
				.map(r -> Map.entry(r.getId().getEditionId(), r))
				.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));

		List<Long> allowedAssignmentsForStudent = new ArrayList<>();
		List<Long> allowedAssignmentsForStaff = new ArrayList<>();

		for (ModuleDetailsDTO module : modules) {
			RolePersonDetailsDTO role = (RolePersonDetailsDTO) allRolesForPersonPerEdition
					.get(module.getEdition().getId());
			if (role != null) {
				List<Long> assignmentsForModule = module.getAssignments().stream()
						.map(AssignmentSummaryDTO::getId).toList();
				if (role.getType().equals(TEACHER) || role.getType().equals(TEACHER_RO)
						|| role.getType().equals(HEAD_TA) || role.getType().equals(TA)) {
					allowedAssignmentsForStaff.addAll(assignmentsForModule);
				} else {
					allowedAssignmentsForStudent.addAll(assignmentsForModule);
				}
			}
		}

		boolean hasMultipleRoles = !allowedAssignmentsForStaff.isEmpty()
				&& !allowedAssignmentsForStudent.isEmpty();

		model.addAttribute("hasMultipleRoles", hasMultipleRoles);
		model.addAttribute("allowedAssignmentsForStaff", allowedAssignmentsForStaff);
		model.addAttribute("allowedAssignmentsForStudent", allowedAssignmentsForStudent);

	}

	/**
	 * Handles any {@link AccessDeniedException} thrown in this controller.
	 *
	 * @param  request The HTTP request associated with this exception.
	 * @return         The enrol page when the exception is thrown from a /lab/* page. Otherwise return the
	 *                 403 error page.
	 */
	@ExceptionHandler(AccessDeniedException.class)
	public String redirectToEnrolPage(HttpServletRequest request) {
		Pattern testPattern = Pattern.compile("/lab/([0-9]+)");
		Matcher matcher = testPattern.matcher(request.getRequestURI());
		if (matcher.matches() && matcher.groupCount() == 1) {
			Optional<QueueSession<?>> lab = qsRepository.findById(Long.parseLong(matcher.group(1)));
			SessionDetailsDTO session = sessionService.getCoreSession(lab.orElseThrow().getSession());
			Long edition = session.getEdition().getId();
			return "redirect:/edition/" + edition + "/enrol";
		}
		return "error/403";
	}

	/**
	 * Gets the presentation page for a lab.
	 *
	 * @param  session The lab
	 * @param  room    The room to present in
	 * @return         The presentation page
	 */
	@GetMapping("/present/{session}")
	public String getLabPresentation(@PathEntity QueueSession<?> session,
			@RequestParam(required = false) Long room, Model model) {
		if (!(session instanceof Lab lab))
			throw new ResourceNotFoundException();

		model.addAttribute("lab", queueSessionViewDTOService.convert(lab));
		model.addAttribute("presentation", presentationViewDTOService.convert(ls.getLabPresentation(lab)));
		if (room != null) {
			model.addAttribute("room", rCache.getRequired(room));
		}

		return "lab/presentation/view";
	}

	/**
	 * Gets the edit page for a lab presentation.
	 *
	 * @param  session The session to edit the presentation for
	 * @return         The edit page
	 */
	@GetMapping("/lab/{session}/presentation/edit")
	@PreAuthorize("@permissionService.canManageSession(#session)")
	public String getLabPresentationEditPage(@PathEntity QueueSession<?> session, Model model) {
		if (!(session instanceof Lab lab))
			throw new ResourceNotFoundException();

		ls.setOrganizationInModel(lab, model);
		model.addAttribute("lab", queueSessionViewDTOService.convert(lab));
		model.addAttribute("presentation", ls.getLabPresentation(lab));

		return "lab/presentation/edit";
	}

	/**
	 * Edits a lab presentation.
	 *
	 * @param  session The session to edit
	 * @param  patch   The patch with the new presentation
	 * @return         Redirect to the lab page
	 */
	@PostMapping("/lab/{session}/presentation/edit")
	@PreAuthorize("@permissionService.canManageSession(#session)")
	public String editLabPresentation(@PathEntity QueueSession<?> session, PresentationPatchDTO patch) {
		if (!(session instanceof Lab lab))
			throw new ResourceNotFoundException();

		csr.deleteAll(lab.getPresentation().getCustomSlides());
		lab.getPresentation().getCustomSlides().clear();
		if (patch.getCustomSlides() != null) {
			List<CustomSlide> customSlides = patch.getCustomSlides().stream().map(it -> it.apply(converter))
					.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);
			customSlides.forEach(s -> s.setPresentation(lab.getPresentation()));
			lab.getPresentation().setCustomSlides(customSlides);
			csr.saveAll(customSlides);
		}
		pr.save(patch.apply(lab.getPresentation(), converter));

		return "redirect:/lab/{session}";
	}

	/**
	 * Endpoint for the status page of an individual lab/session
	 *
	 * @param  qSession The lab/session we are interested about.
	 * @param  model    The model to fill
	 * @return          The view name
	 */
	@GetMapping("/lab/{qSession}/status")
	@PreAuthorize("@permissionService.canManageSession(#qSession)")
	public String status(@PathEntity QueueSession<?> qSession,
			Model model) {
		SessionDetailsDTO session = sessionService.getSessionDTOFromSession(qSession);

		if (session.getEdition() != null) {
			model.addAttribute("edition", eCache.getRequired(session.getEdition().getId()));
		} else {
			model.addAttribute("ec", ecCache.getRequired(session.getEditionCollection().getId()));
			model.addAttribute("editionsToCourses", session.getEditions().stream()
					.collect(Collectors.toMap(EditionSummaryDTO::getId,
							edition -> eCache.getRequired(edition.getId()).getCourse())));
		}

		model.addAttribute("qSession", queueSessionViewDTOService.convert(qSession));
		model.addAttribute("rooms",
				rCache.getAndIgnoreMissing(session.getRooms().stream().map(RoomDetailsDTO::getId)));
		model.addAttribute("assignments",
				aCache.getAndIgnoreMissing(
						session.getAssignments().stream().map(AssignmentSummaryDTO::getId)));
		return "lab/view/status";
	}

}
