/*
 * 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 java.time.LocalDateTime.now;
import static nl.tudelft.librador.util.PageUtil.toPage;

import java.io.IOException;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
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 org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import jakarta.servlet.http.HttpServletResponse;
import jakarta.transaction.Transactional;
import jakarta.validation.Valid;
import nl.tudelft.labracore.api.*;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
import nl.tudelft.labracore.lib.security.user.DefaultRole;
import nl.tudelft.labracore.lib.security.user.Person;
import nl.tudelft.librador.dto.DTOConverter;
import nl.tudelft.librador.util.PageUtil;
import nl.tudelft.queue.cache.*;
import nl.tudelft.queue.csv.EmptyCsvException;
import nl.tudelft.queue.csv.InvalidCsvException;
import nl.tudelft.queue.csv.InvalidCsvValuesException;
import nl.tudelft.queue.dto.create.CourseRequestCreateDTO;
import nl.tudelft.queue.dto.create.QueueEditionCreateDTO;
import nl.tudelft.queue.dto.create.QueueRoleCreateDTO;
import nl.tudelft.queue.dto.util.EditionFilterDTO;
import nl.tudelft.queue.dto.view.QueueEditionDetailsDTO;
import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.QueueEdition;
import nl.tudelft.queue.model.enums.QueueSessionType;
import nl.tudelft.queue.model.labs.Lab;
import nl.tudelft.queue.repository.QueueEditionRepository;
import nl.tudelft.queue.repository.QueueSessionRepository;
import nl.tudelft.queue.service.*;
import nl.tudelft.queue.service.dto.QueueSessionViewDTOService;

@Controller
public class EditionController {

	@Autowired
	private QueueSessionRepository lr;

	@Autowired
	private CohortControllerApi cohortApi;

	@Autowired
	private EditionControllerApi eApi;

	@Autowired
	private PersonControllerApi pApi;

	@Autowired
	private RoleControllerApi rApi;

	@Autowired
	private CourseControllerApi cApi;

	@Autowired
	private AssignmentCacheManager aCache;

	@Autowired
	private QuestionControllerApi qApi;

	@Autowired
	private ModuleControllerApi mApi;

	@Autowired
	private CourseCacheManager cCache;

	@Autowired
	private EditionCacheManager eCache;

	@Autowired
	private EditionRolesCacheManager erCache;

	@Autowired
	private ModuleCacheManager mCache;

	@Autowired
	private RoleCacheManager rCache;

	@Autowired
	private RoomCacheManager rmCache;

	@Autowired
	private SessionCacheManager sCache;

	@Autowired
	private StudentGroupCacheManager sgCache;

	@Autowired
	private EditionService es;

	@Autowired
	private EditionStatusService ess;

	@Autowired
	private PermissionService ps;

	@Autowired
	private RoleDTOService roleDTOService;

	@Autowired
	private QuestionService qs;

	@Autowired
	private QueueEditionRepository qer;

	@Autowired
	private SessionService sessionService;

	@Autowired
	private QueueSessionViewDTOService queueSessionViewDTOService;

	@Autowired
	private DTOConverter converter;

	/**
	 * Gets the page listing all editions. This page incorporates pageables to index many different editions
	 * and keep the overview small.
	 *
	 * @param  person   The currently authenticated person asking for the editions page.
	 * @param  pageable The pageable object representing the current page and size to display.
	 * @param  model    The model to fill out for Thymeleaf template resolution.
	 * @return          The Thymeleaf template to resolve.
	 */
	@GetMapping("/editions")
	public String getEditionList(@AuthenticatedPerson Person person,
			@PageableDefault(sort = "id", direction = Sort.Direction.DESC) Pageable pageable,
			Model model) {

		PersonDetailsDTO pd = Objects.requireNonNull(pApi.getPersonById(person.getId()).block());

		var filter = es.getFilter("/editions");
		List<EditionDetailsDTO> lcEditions = eApi.getAllEditionsActiveOrTaughtBy(person.getId()).collectList()
				.block();
		eCache.register(lcEditions);
		erCache.getAndIgnoreMissing(lcEditions.stream().map(EditionDetailsDTO::getId));

		var editions = es.queueEditionDTO(es.filterEditions(lcEditions, filter),
				QueueEditionDetailsDTO.class);
		if (person.getDefaultRole() != DefaultRole.ADMIN) {
			editions = editions.stream().filter(e -> !e.getHidden()).toList();
		}
		var page = PageUtil.toPage(pageable, editions, Comparator.comparing(QueueEditionDetailsDTO::getId));

		model.addAttribute("editions", page);
		model.addAttribute("programs", cCache.getAll()
				.stream().map(CourseDetailsDTO::getProgram).distinct()
				.sorted(Comparator.comparing(ProgramSummaryDTO::getName))
				.collect(Collectors.toList()));
		model.addAttribute("filter", filter);

		model.addAttribute("page", "catalog");

		return "edition/index";
	}

	/**
	 * Submits the programme filters for the editions page.
	 *
	 * @param  programs The programmes to filter on
	 * @return          The page to load
	 */
	@PostMapping("/editions/filter")
	public String submitFilters(@RequestParam(required = false) List<Long> programs,
			@RequestParam(required = false) String nameSearch,
			RedirectAttributes redirectAttributes) {
		es.storeFilter(new EditionFilterDTO(programs, nameSearch), "/editions");
		redirectAttributes.addAttribute("page", 0);
		return "redirect:/editions";
	}

	/**
	 * Gets the page for teachers to fill out when requesting a new course to be made.
	 *
	 * @param  model The model to fill out for Thymeleaf template resolution.
	 * @return       The Thymeleaf template to resolve.
	 */
	@GetMapping("/editions/request-course")
	@PreAuthorize("@permissionService.isAdminOrTeacher()")
	public String getRequestCoursePage(Model model) {
		model.addAttribute("dto", new CourseRequestCreateDTO());

		return "course/request";
	}

	/**
	 * Gets the confirmation page for users that have the intention of enrolling in the given course edition.
	 * Entrance into this page is given whenever the authenticated person does not have a role in the given
	 * edition yet. If the person is already enrolled, they will be redirected to the catalog page.
	 *
	 * @param  editionId The id of the edition the user wants to enrol in.
	 * @param  model     The model to fill out for Thymeleaf template resolution.
	 * @return           The Thymeleaf template to resolve.
	 */
	@GetMapping("/edition/{editionId}/enrol")
	@PreAuthorize("@permissionService.canEnrolForEdition(#editionId) " +
			"|| @permissionService.canViewEdition(#editionId)")
	public String getEditionEnrolView(@PathVariable Long editionId, Model model) {
		if (!ps.canEnrolForEdition(editionId)) {
			return "redirect:/editions";
		}
		model.addAttribute("edition", eCache.getRequired(editionId));

		return "edition/enrol";
	}

	/**
	 * Posts an enrolment request for one person in one edition. This method handles the post by calling the
	 * appropriate enrolment function at Labracore.
	 *
	 * @param  person    The currently authenticated person that is trying to enrol for a course edition.
	 * @param  editionId The id of the edition the user wants to enrol in.
	 * @return           A redirect to the edition info page for the given edition edition.
	 */
	@PostMapping("/edition/{editionId}/enrol")
	@PreAuthorize("@permissionService.canEnrolForEdition(#editionId)")
	public String enrolForEdition(@AuthenticatedPerson Person person,
			@PathVariable Long editionId) {
		eApi.addStudentsToEdition(List.of(person.getUsername()), editionId, false).block();

		return "redirect:/edition/" + editionId;
	}

	/**
	 * Gets the main course edition info panel page. This page shows the most generic information on a course
	 * edition taken from Labracore.
	 *
	 * @param  editionId The id of the edition to display.
	 * @param  model     The model to fill out for Thymeleaf template resolution.
	 * @return           The Thymeleaf template to resolve.
	 */
	@GetMapping("/edition/{editionId}")
	@PreAuthorize("@permissionService.canViewEdition(#editionId) || @permissionService.canEnrolForEdition(#editionId)")
	public String getEditionView(@PathVariable Long editionId, Model model) {
		QueueEditionDetailsDTO edition = es.queueEditionDTO(eCache.getRequired(editionId),
				QueueEditionDetailsDTO.class);

		model.addAttribute("edition", edition);
		model.addAttribute("assignments",
				mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId))
						.stream()
						.flatMap(m -> m.getAssignments().stream()).toList());

		return "edition/view/info";
	}

	/**
	 * Gets the course edition info panel on participants. This page should be shown to people that have
	 * permission to edit the participants list and can see all students.
	 *
	 * @param  editionId The id of the edition for which to display participants.
	 * @param  model     The model to fill out for Thymeleaf template resolution.
	 * @return           The Thymeleaf template to resolve.
	 */
	@GetMapping("/edition/{editionId}/participants")
	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
	public String getEditionParticipantsView(@PathVariable Long editionId, Model model,
			@RequestParam(value = "student-search", required = false) String studentSearch,
			@PageableDefault(size = 25) Pageable pageable) {
		var edition = eCache.getRequired(editionId);
		var students = roleDTOService.students(edition);

		if (studentSearch != null) {
			students = es.studentsMatchingFilter(students, studentSearch);
		}

		model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class));
		model.addAttribute("assignments",
				mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId))
						.stream()
						.flatMap(m -> m.getAssignments().stream()).toList());
		model.addAttribute("students",
				toPage(pageable, students, Comparator.comparing(PersonSummaryDTO::getDisplayName)));

		return "edition/view/participants";
	}

	/**
	 * Gets the course edition info panel on modules. This page should be shown to all students and
	 * assistants. This page only displays information on the modules, assignments and groups as can be found
	 * in Labracore.
	 *
	 * @param  editionId The id of the edition for which to display the modules.
	 * @param  model     The model to fill out for Thymeleaf template resolution.
	 * @return           The Thymeleaf template to resolve.
	 */
	@GetMapping("/edition/{editionId}/modules")
	@PreAuthorize("@permissionService.canViewEdition(#editionId)")
	public String getEditionModulesView(@PathVariable Long editionId, Model model) {
		var edition = eCache.getRequired(editionId);
		var modules = mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId));

		modules.sort(Comparator.comparing(ModuleDetailsDTO::getName));
		modules.forEach(m -> {
			m.getGroups().sort(Comparator.comparing(StudentGroupSmallSummaryDTO::getName));
			m.getAssignments().sort(Comparator.comparing(AssignmentSummaryDTO::getName));
		});

		sgCache.getAndIgnoreMissing(modules.stream()
				.flatMap(m -> m.getGroups().stream().map(StudentGroupSmallSummaryDTO::getId)));

		model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class));
		model.addAttribute("modules", modules);
		model.addAttribute("assignments",
				modules.stream().flatMap(m -> m.getAssignments().stream()).toList());
		model.addAttribute("groups", modules.stream()
				.collect(Collectors.toMap(ModuleDetailsDTO::getId,
						m -> sgCache.getAndIgnoreMissing(
								m.getGroups().stream().map(StudentGroupSmallSummaryDTO::getId)))));

		return "edition/view/modules";
	}

	/**
	 * Gets the course edition info panel on labs. This page should shown to all those that need to interact
	 * with labs. This page shows all labs currently available and has links to view all labs and a button to
	 * create a new lab.
	 *
	 * @param  editionId The id of the edition for which to display labs.
	 * @param  model     The model to fill out for Thymeleaf template resolution.
	 * @return           The Thymeleaf template to resolve.
	 */
	@GetMapping("/edition/{editionId}/labs")
	@PreAuthorize("@permissionService.canViewEdition(#editionId)")
	public String getEditionSessionsView(@PathVariable Long editionId,
			@RequestParam(required = false, defaultValue = "") List<QueueSessionType> queueSessionTypes,
			@RequestParam(required = false, defaultValue = "") List<Long> modules,
			Model model) {
		EditionDetailsDTO edition = eCache.getRequired(editionId);

		// Make sure that session details are cached.
		List<SessionDetailsDTO> sessions = sessionService
				.getCoreSessions(edition.getSessions().stream().map(SessionSummaryDTO::getId).toList());

		// Find all labs from the sessions, convert to summaries, and filter.
		List<QueueSessionSummaryDTO> labs = es.filterLabs(
				getLabSummariesFromSessions(sessions, s -> true),
				queueSessionTypes, modules);

		// Sort all labs
		labs = es.sortLabs(labs);

		model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class));
		model.addAttribute("labs", labs);
		model.addAttribute("allLabTypes", QueueSessionType.values());
		model.addAttribute("queueSessionTypes", queueSessionTypes);
		model.addAttribute("allModules", edition.getModules());
		model.addAttribute("modules", modules);
		model.addAttribute("assignments",
				mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId))
						.stream()
						.flatMap(m -> m.getAssignments().stream()).toList());

		return "edition/view/labs";
	}

	/**
	 * Gets the course edition question page.
	 *
	 * @param  editionId The id of the edition to display.
	 * @param  model     The model to fill out for Thymeleaf template resolution.
	 * @return           The Thymeleaf template to resolve.
	 */
	@GetMapping("/edition/{editionId}/questions")
	@PreAuthorize("@permissionService.canManageQuestions(#editionId)")
	public String getEditionQuestions(@PathVariable Long editionId, Model model) {
		EditionDetailsDTO edition = eCache.getRequired(editionId);

		model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class));
		model.addAttribute("assignments",
				mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId))
						.stream()
						.flatMap(m -> m.getAssignments().stream()).toList());
		model.addAttribute("questions", qApi.getQuestionsByEdition(editionId).collectList().block());

		return "edition/view/questions";
	}

	/**
	 * Gets the participant add page. This page is used to add new participants to an edition.
	 *
	 * @param  editionId The id of the edition to which a participant should be added.
	 * @param  model     The model to fill out for Thymeleaf template resolution.
	 * @return           The Thymeleaf template to resolve.
	 */
	@GetMapping("/edition/{editionId}/participants/create")
	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
	public String getAddParticipantPage(@PathVariable Long editionId, Model model) {
		EditionDetailsDTO edition = eCache.getRequired(editionId);

		model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class));
		model.addAttribute("assignments",
				mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId))
						.stream()
						.flatMap(m -> m.getAssignments().stream()).toList());
		model.addAttribute("role", new QueueRoleCreateDTO(editionId));

		return "edition/create/participant";
	}

	/**
	 * Posts a single participant addition to the server. This action comes from the participant add page.
	 * Does not let a teacher demote themselves if they are the only one in the edition, but it can be done
	 * with a warning if there are multiple teachers
	 *
	 * @param  editionId The id of the edition to which a participant should be added.
	 * @param  model     The model to fill out for Thymeleaf template resolution.
	 * @return           The Thymeleaf template to resolve.
	 */
	@PostMapping("/edition/{editionId}/participants/create")
	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
	public String createParticipant(@PathVariable Long editionId, Model model,
			QueueRoleCreateDTO dto, RedirectAttributes redirectAttributes) {
		// TODO: Figure out a way to get rid of these additional sets
		dto.setEditionId(editionId);
		EditionDetailsDTO edition = eCache.getRequired(editionId);

		RoleCreateDTO create = dto.apply(converter);
		if (dto.hasErrors()) {
			model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class));
			model.addAttribute("role", dto);
			model.addAttribute("assignments",
					mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId))
							.stream()
							.flatMap(m -> m.getAssignments().stream()).toList());

			return "edition/create/participant";
		}

		try {
			PersonDetailsDTO person = pApi.getPersonByUsername(dto.getIdentifier()).block();
			return checkDemoteThenAddSingle(person, edition, redirectAttributes, create, model);

		} catch (WebClientResponseException e) {
			redirectAttributes.addFlashAttribute("error",
					String.format("Person with username: %s was not added",
							dto.getIdentifier()));
			return "redirect:/edition/" + editionId + "/participants/create";
		}
	}

	@GetMapping("/edition/{editionId}/search-people")
	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
	public String searchForPeople(@PathVariable Long editionId, @RequestParam String identifier,
			Model model) {
		if (identifier.length() < 3) {
			model.addAttribute("people", Collections.emptyList());
		} else {
			int pageNumber = 0;
			int pageSize = 10;
			List<PersonSummaryDTO> people = pApi.searchForPeopleByIdentifier(identifier, pageNumber, pageSize)
					.collectList().block();
			model.addAttribute("people", people);
		}
		model.addAttribute("editionId", editionId);
		return "edition/view/people-search";
	}

	/**
	 * Checks if the added participant will be demoted, prevents it if the person is the only teacher of the
	 * course, allows it with a warning if there are multiple teachers
	 *
	 * @param  person             the PersonDetailsDTO with participant details
	 * @param  edition            the edition the participant will be added in
	 * @param  redirectAttributes the RedirectedAttributes used for flash attributes
	 * @param  create             participant creation details
	 * @return                    theThymeleaf template to resolve
	 */
	private String checkDemoteThenAddSingle(PersonDetailsDTO person, EditionDetailsDTO edition,
			RedirectAttributes redirectAttributes, RoleCreateDTO create, Model model) {
		Long editionId = edition.getId();

		// not a teacher or not given a low role, no demote
		if (!roleDTOService.isTeacherInEdition(person.getId(), edition)
				|| create.getType().equals(RoleCreateDTO.TypeEnum.TEACHER)) {
			rApi.addRole(create.person(new PersonIdDTO().id(person.getId()))).block();
			return "redirect:/edition/" + editionId + "/participants";
		}
		// do not demote if teacher is the only one
		if (roleDTOService.isTheOnlyTeacherInEdition(person.getId(), edition)) {
			redirectAttributes.addFlashAttribute("error",
					"You can not assign yourself a lower role than you already have as a teacher. ");
			return "redirect:/edition/" + editionId + "/participants/create";
		}

		model.addAttribute("role", create);
		model.addAttribute("person", person);
		model.addAttribute("edition", edition);

		return "edition/view/demote";

	}

	/**
	 * Demotes a teacher's role to a lower one in the given edition
	 *
	 * @param  editionId The id of the edition from which the teacher's role would be changed
	 * @param  personId  The id of the person that is being given a lower role
	 * @return           The Thymeleaf template to resolve.
	 */
	@PostMapping("/edition/{editionId}/participants/{personId}/demote/{newRole}")
	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
	public String demoteParticipant(@PathVariable Long editionId, @PathVariable Long personId,
			RoleCreateDTO role) {
		rApi.addRole(role.person(new PersonIdDTO().id(personId)).edition(new EditionIdDTO().id(editionId)))
				.block();

		return "redirect:/edition/" + editionId + "/participants";
	}

	/**
	 * Adds new participants to the course based on a CSV file that was uploaded by the user.
	 *
	 * @param  editionId  The id of the edition to which the users must be added.
	 * @param  csv        A CSV file containing both the netId and their role in the course.
	 * @param  attributes Used to pass information when redirecting to Thymeleaf.
	 * @return            A redirect to either the participants page when successfull, otherwise redirect ot
	 *                    create participants page
	 */
	@PostMapping("/edition/{editionId}/participants/import")
	@PreAuthorize("@permissionService.canManageTeachers(#editionId)")
	public String importParticipants(@PathVariable Long editionId,
			@RequestParam("file") MultipartFile csv,
			RedirectAttributes attributes) {
		EditionDetailsDTO edition = eCache.getRequired(editionId);

		try {
			es.addCourseParticipants(csv, edition);
			return "redirect:/edition/" + editionId + "/participants";
		} catch (EmptyCsvException | InvalidCsvValuesException | InvalidCsvException e) {
			attributes.addFlashAttribute("error", e.getMessage());
		}

		return "redirect:/edition/" + editionId + "/participants/create";
	}

	/**
	 * Gets the participant remove page. This page is used to remove a specific participant from the given
	 * edition.
	 *
	 * @param  editionId The id of the edition from which the participant should be removed.
	 * @param  personId  The id of the person who should be removed.
	 * @param  model     The model to fill out for Thymeleaf template resolution.
	 * @return           The Thymeleaf template to resolve.
	 */
	@GetMapping("/edition/{editionId}/participants/{personId}/remove")
	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
	public String getRemoveParticipantPage(@PathVariable Long editionId, @PathVariable Long personId,
			Model model) {
		EditionDetailsDTO edition = eCache.getRequired(editionId);

		model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class));
		model.addAttribute("assignments",
				mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId))
						.stream()
						.flatMap(m -> m.getAssignments().stream()).toList());
		model.addAttribute("role", rCache.getRequired(new RoleId()
				.personId(personId).editionId(editionId)));

		return "edition/remove/participant";
	}

	/**
	 * Removes a participant with the given personId from the given edition.
	 *
	 * @param  editionId The id of the edition from which the participant should be removed.
	 * @param  personId  The id of the person who should be removed.
	 * @return           A redirect to the edition participants overview.
	 */
	@PostMapping("/edition/{editionId}/participants/{personId}/remove")
	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
	public String removeParticipant(@PathVariable Long editionId, @PathVariable Long personId) {

		eApi.removePersonFromEdition(editionId, personId).block();

		return "redirect:/edition/" + editionId + "/participants";
	}

	/**
	 * Blocks a person with the given id from the given edition.
	 *
	 * @param  editionId The id of the edition to block someone from.
	 * @param  personId  The id of the person to block.
	 * @return           A redirect to the edition participants overview.
	 */
	@PostMapping("/edition/{editionId}/participants/{personId}/block")
	@PreAuthorize("@permissionService.canManageParticipants(#editionId)")
	public String blockParticipant(@PathVariable Long editionId, @PathVariable Long personId) {
		eApi.blockPersonInEdition(editionId, personId).block();

		return "redirect:/edition/" + editionId + "/participants";
	}

	/**
	 * Processes a POST request from the user wanting to leave the given edition. Instead of deleting the role
	 * of the user from the database entirely, we opt to block their role and thus disallow them access to the
	 * edition.
	 *
	 * @param  user      The currently authenticated user as a Person object.
	 * @param  editionId The id of the edition that the student wants to leave.
	 * @return           A redirect back to the edition view page.
	 */
	@PostMapping("/edition/{editionId}/leave")
	@PreAuthorize("@permissionService.canLeaveEdition(#editionId)")
	public String participantLeave(@AuthenticatedPerson Person user,
			@PathVariable Long editionId) {
		eApi.removePersonFromEdition(editionId, user.getId()).block();

		return "redirect:/edition/" + editionId;
	}

	/**
	 * Gets the archive edition page to confirm that the teacher wants to archive the edition.
	 *
	 * @param  editionId The id of the edition to archive.
	 * @return           The Thymeleaf template to resolve.
	 */
	@GetMapping("/edition/{editionId}/archive")
	@PreAuthorize("@permissionService.canManageEdition(#editionId)")
	public String getArchiveEdition(@PathVariable Long editionId,
			Model model) {
		EditionDetailsDTO edition = eCache.getRequired(editionId);
		model.addAttribute("edition", edition);
		return "edition/view/archive";
	}

	/**
	 * Archives the edition
	 *
	 * @param  editionId The id of the edition to archive.
	 * @return           A redirect back to the edition view page.
	 */
	@PostMapping("/edition/{editionId}/archive")
	@PreAuthorize("@permissionService.canManageEdition(#editionId)")
	public String archiveEdition(@PathVariable Long editionId) {
		eApi.archiveEdition(editionId).block();

		return "redirect:/edition/" + editionId;
	}

	/**
	 * Gets the edition status page. This page is a status overview for the entire edition.
	 *
	 * @param  editionId The id of the edition for which to view the edition status.
	 * @param  model     The model to fill out for Thymeleaf template resolution.
	 * @return           The Thymeleaf template to resolve.
	 */
	@GetMapping("/edition/{editionId}/status")
	@PreAuthorize("@permissionService.canViewEditionStatus(#editionId)")
	public String status(@PathVariable Long editionId,
			Model model) {
		EditionDetailsDTO edition = eCache.getRequired(editionId);
		List<SessionDetailsDTO> sessions = sessionService
				.getCoreSessions(edition.getSessions().stream().map(SessionSummaryDTO::getId).toList());

		model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class));
		model.addAttribute("assignments",
				mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId))
						.stream()
						.flatMap(m -> m.getAssignments().stream()).toList());

		model.addAttribute("activeLabs", getLabSummariesFromSessions(
				sessions, s -> s.getStart().isBefore(now()) && now().isBefore(s.getEndTime())));
		model.addAttribute("inactiveLabs", getLabSummariesFromSessions(
				sessions, s -> !(s.getStart().isBefore(now()) && now().isBefore(s.getEndTime()))));

		model.addAttribute("rooms", rmCache.getAndIgnoreMissing(sessions.stream()
				.flatMap(s -> s.getRooms().stream().map(RoomDetailsDTO::getId)).distinct()));

		return "edition/view/status";
	}

	/**
	 * Gets the table containing the amount of requests each TA handled during the given list of labs.
	 *
	 * @param  editionId The id of the edition the status page is viewed of.
	 * @param  labs      The list of labs whose requests should be counted.
	 * @param  model     The Thymeleaf model to fill out.
	 * @return           The part of the Thymeleaf template to resolve.
	 */
	@GetMapping("/edition/{editionId}/status/freq/assistant")
	@PreAuthorize("@permissionService.canViewEditionStatus(#editionId)")
	public String getAssistantCounts(@PathVariable Long editionId,
			@RequestParam(required = false, defaultValue = "") List<Lab> labs,
			Model model) {
		List<LabRequest> requests = ess.getFilteredRequests(labs);
		model.addAttribute("assistants", ess.ratingAndRequestsForAssistant(requests));
		return "edition/view/status :: #assistant-table";
	}

	/**
	 *
	 * @param  model The model to fill out for Thymeleaf template resolution.
	 * @return       The thymeleaf template to resolve.
	 */
	@GetMapping("/edition/add")
	@PreAuthorize("@permissionService.canCreateEdition()")
	public String viewAddEdition(@AuthenticatedPerson Person person, Model model) {
		QueueEditionCreateDTO edition = QueueEditionCreateDTO.builder().build();
		model.addAttribute("edition", edition);

		List<CourseSummaryDTO> courses = cApi.getAllCoursesByManager(person.getId()).collectList().block();
		model.addAttribute("courses", courses);

		List<CohortSummaryDTO> cohorts = cohortApi.getAllCohorts().collectList().block();
		model.addAttribute("cohorts", cohorts);

		return "edition/create";
	}

	/**
	 * Creates a new edition for a specific course.
	 *
	 * @param  create The DTO used to create the edition
	 * @return        Redirect to the course page.
	 */
	@PostMapping("/edition/add")
	@PreAuthorize("@permissionService.canCreateEdition(#create.course)")
	public String addEdition(@Valid @ModelAttribute("edition") QueueEditionCreateDTO create) {
		create.setEnrollability(EditionCreateDTO.EnrollabilityEnum.OPEN);
		EditionCreateDTO edition = create.apply(converter);

		Long id = eApi.addEdition(edition).block();
		qer.save(QueueEdition.builder().id(id).hidden(false).build());

		return "redirect:/edition/" + id;
	}

	/**
	 * Searches for questions when creating a request.
	 *
	 * @param  editionId  The id of the edition of the request
	 * @param  assignment The id of the assignment of the request
	 * @param  q          The search query
	 * @return            The resulting list of questions
	 */
	@ResponseBody
	@GetMapping("/edition/{editionId}/questions/search")
	public List<QuestionDetailsDTO> searchForQuestions(@PathVariable Long editionId,
			@RequestParam(required = false) Long assignment, @RequestParam String q) {
		return qs.searchForQuestions(q, editionId, assignment, 5);
	}

	/**
	 * Adds one to the useful count of a question.
	 *
	 * @param  editionId  The id of the edition the question is for
	 * @param  questionId The id of the question
	 * @return            The page to load
	 */
	@PostMapping("/edition/{editionId}/questions/{questionId}/vote")
	public String giveUsefulVote(@PathVariable Long editionId, @PathVariable Long questionId) {
		qApi.giveUsefulVote(questionId).block();
		return "redirect:/edition/{editionId}";
	}

	/**
	 * Gets the page to edit a question.
	 *
	 * @param  editionId  The id of the edition the question is for
	 * @param  questionId The id of the question
	 * @param  model      The model to add details to
	 * @return            The page to load
	 */
	@GetMapping("/edition/{editionId}/questions/{questionId}/edit")
	@PreAuthorize("@permissionService.canManageQuestions(#editionId)")
	public String getEditQuestionPage(@PathVariable Long editionId, @PathVariable Long questionId,
			Model model) {
		EditionDetailsDTO edition = eCache.getRequired(editionId);

		model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class));
		model.addAttribute("assignments",
				mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId))
						.stream()
						.flatMap(m -> m.getAssignments().stream()).toList());
		model.addAttribute("question", qApi.getQuestionById(questionId).block());
		model.addAttribute("assignments",
				mApi.getModulesById(edition.getModules().stream().map(ModuleSummaryDTO::getId)
						.collect(Collectors.toList())).collectList().block().stream()
						.flatMap(m -> m.getAssignments().stream()).collect(Collectors.toList()));
		model.addAttribute("dto", new QuestionPatchDTO().assignment(new AssignmentIdDTO()));

		return "edition/edit/question";
	}

	/**
	 * Edits a question.
	 *
	 * @param  editionId  The id of the edition the question is for
	 * @param  questionId The id of the question
	 * @param  dto        The DTO with patch data
	 * @return            The page to load
	 */
	@PostMapping("/edition/{editionId}/questions/{questionId}/edit")
	@PreAuthorize("@permissionService.canManageQuestions(#editionId)")
	public String editQuestion(@PathVariable Long editionId, @PathVariable Long questionId,
			QuestionPatchDTO dto) {
		qApi.patchQuestion(dto, questionId).block();

		return "redirect:/edition/{editionId}/questions";
	}

	/**
	 * Gets the delete question page
	 *
	 * @param  editionId  The id of the edition the question is for
	 * @param  questionId The id of the question
	 * @param  model      The model to add details to
	 * @return            The page to load
	 */
	@GetMapping("/edition/{editionId}/questions/{questionId}/remove")
	@PreAuthorize("@permissionService.canManageQuestions(#editionId)")
	public String getDeleteQuestionPage(@PathVariable Long editionId, @PathVariable Long questionId,
			Model model) {
		EditionDetailsDTO edition = eCache.getRequired(editionId);

		model.addAttribute("edition", es.queueEditionDTO(edition, QueueEditionDetailsDTO.class));
		model.addAttribute("assignments",
				mCache.getAndIgnoreMissing(edition.getModules().stream().map(ModuleSummaryDTO::getId))
						.stream()
						.flatMap(m -> m.getAssignments().stream()).toList());
		model.addAttribute("question", qApi.getQuestionById(questionId).block());

		return "edition/remove/question";
	}

	/**
	 * Deletes a question.
	 *
	 * @param  editionId  The id of the edition the question is for
	 * @param  questionId The id of the question
	 * @return            The page to load
	 */
	@PostMapping("/edition/{editionId}/questions/{questionId}/remove")
	@PreAuthorize("@permissionService.canManageQuestions(#editionId)")
	public String deleteQuestion(@PathVariable Long editionId, @PathVariable Long questionId) {
		qApi.deleteQuestion(questionId).block();

		return "redirect:/edition/{editionId}/questions";
	}

	/**
	 * Exports all the requests of all the labs in an edition to a zip file.
	 *
	 * @param  editionId   The id of the edition for which this should be exported.
	 * @param  response    The response containng the newly created zip file.
	 * @throws IOException
	 */
	@GetMapping("/edition/{editionId}/requests/export")
	@PreAuthorize("@permissionService.canManageEdition(#editionId)")
	public void exportEdition(@PathVariable Long editionId, HttpServletResponse response) throws IOException {
		response.setContentType("application/octet-stream");
		response.setHeader("Content-Disposition", "attachment;filename=export-edition"
				+ editionId + ".zip");
		response.setStatus(HttpServletResponse.SC_OK);
		es.editionToZip(editionId, response);
	}

	/**
	 * Exports the latest submission requests of an edition.
	 *
	 * @param  editionId   The id of the edition for which this should be exported
	 * @return
	 * @throws IOException
	 */
	@GetMapping("/edition/{editionId}/requests/signofflist.csv")
	@PreAuthorize("@permissionService.canManageEdition(#editionId)")
	public ResponseEntity<Resource> exportSubmissions(@PathVariable Long editionId) throws IOException {
		var resource = es.submissionLabsToCsv(editionId);

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

	/**
	 * Gets lab views for the given list of sessions after applying the given predicate.
	 *
	 * @param  sessions The sessions that are to be displayed through lab view DTOs.
	 * @param  p        The predicate to apply to the sessions to filter out the relevant labs.
	 * @return          The list of LabViewDTOs representing the selected sessions (and labs).
	 */
	private List<QueueSessionSummaryDTO> getLabSummariesFromSessions(List<SessionDetailsDTO> sessions,
			Predicate<SessionDetailsDTO> p) {
		return lr.findAllBySessions(sessions.stream().filter(p).map(SessionDetailsDTO::getId).toList())
				.stream().map(queueSessionViewDTOService::convertToSummary).toList();
	}

	/**
	 * Toggles the visibility of an edition in Queue.
	 *
	 * @param  editionId The id of the edition
	 * @return           A redirect to the edition page
	 */
	@Transactional
	@PostMapping("/edition/{editionId}/visibility")
	@PreAuthorize("@permissionService.canManageEdition(#editionId)")
	public String toggleVisibility(@PathVariable Long editionId) {
		QueueEdition qEdition = es.getOrCreateQueueEdition(editionId);
		qEdition.setHidden(!qEdition.getHidden());
		qer.save(qEdition);
		return "redirect:/edition/{editionId}";
	}

}
