/*
 * 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 nl.tudelft.labracore.api.dto.RoleDetailsDTO.TypeEnum.*;

import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.jooq.lambda.function.Function2;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;

import nl.tudelft.labracore.api.AuthorizationControllerApi;
import nl.tudelft.labracore.api.CourseControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.labracore.lib.security.LabradorUserDetails;
import nl.tudelft.labracore.lib.security.user.DefaultRole;
import nl.tudelft.labracore.lib.security.user.Person;
import nl.tudelft.queue.cache.*;
import nl.tudelft.queue.model.*;
import nl.tudelft.queue.model.enums.QueueSessionType;
import nl.tudelft.queue.model.enums.RequestStatus;
import nl.tudelft.queue.repository.*;
import nl.tudelft.queue.service.dto.RequestViewDTOService;
import reactor.core.publisher.Mono;

@Service
public class PermissionService {

	private static final Set<RoleDetailsDTO.TypeEnum> STAFF_ROLES = Set.of(TA, HEAD_TA, TEACHER, TEACHER_RO);
	private static final Set<RoleDetailsDTO.TypeEnum> NON_MANAGER_ROLES = Set.of(STUDENT, TA);
	private static final Set<RoleDetailsDTO.TypeEnum> MANAGER_ROLES = Set.of(HEAD_TA, TEACHER);

	@Autowired
	private QueueSessionRepository qsr;

	@Autowired
	private LabRepository lr;

	@Autowired
	private RequestRepository rr;

	@Autowired
	private LabRequestRepository lrr;

	@Autowired
	@Lazy
	private LabService ls;

	@Autowired
	private PersonCacheManager pCache;

	@Autowired
	private AssignmentCacheManager aCache;

	@Autowired
	private ModuleCacheManager mCache;

	@Autowired
	private EditionCacheManager eCache;

	@Autowired
	private EditionCollectionCacheManager ecCache;

	@Autowired
	private RoleCacheManager rCache;

	@Autowired
	private SessionCacheManager sCache;

	@Autowired
	private StudentGroupCacheManager sgCache;

	@Autowired
	private AuthorizationControllerApi aApi;

	@Autowired
	private CourseControllerApi cApi;

	@Autowired
	@Lazy
	private RequestService rs;

	@Autowired
	private RoleDTOService rds;

	@Autowired
	private LabStatusService labStatusService;

	@Autowired
	private LabRequestConstraintService labRequestConstraintService;

	@Autowired
	private ModelMapper modelMapper;
	@Autowired
	private RequestViewDTOService requestViewDTOService;

	/**
	 * Gets the Person associated to the currently authenticated entity and passes it to the given callback.
	 * If no API Key with user could be found, this function just outputs {@code false}.
	 *
	 * @param  f The callback function to call with the current {@link Person} to find whether the key is to
	 *           be authorized.
	 * @return   {@code false} either when custom permission was not granted or no current key could be found.
	 */
	private boolean withAuthenticatedUser(Function<Person, Boolean> f) {
		Object principal = SecurityContextHolder.getContext()
				.getAuthentication()
				.getPrincipal();

		if (principal instanceof LabradorUserDetails) {
			return f.apply(((LabradorUserDetails) principal).getUser());
		}

		return false;
	}

	/**
	 * Gets the Person associated to the currently authenticated entity and passes it to the given callback.
	 * If no API Key with user could be found, this function just outputs {@code false}.
	 *
	 * @param  f The callback function to call with the current {@link Person} to find whether the key is to
	 *           be authorized.
	 * @return   {@code false} either when custom permission was not granted or no current key could be found.
	 */
	private boolean withAuthenticatedUserMono(Function<Person, Mono<Boolean>> f) {
		return withAuthenticatedUser(person -> f.apply(person).blockOptional().orElse(false));
	}

	/**
	 * Checks whether a predicate holds given the request with the given id. If no such request could be
	 * found, this function just outputs {@code false}.
	 *
	 * @param  requestId The id of the request to find.
	 * @param  f         The predicate to check.
	 * @return           The outcome of the predicate, or false if no request with the given id could be
	 *                   found.
	 */
	private boolean withRequest(Long requestId, Function<Request<?>, Boolean> f) {
		return rr.findById(requestId).map(f).orElse(false);
	}

	/**
	 * Checks whether a predicate holds given the lab request with the given id. If no such lab request could
	 * be found, this function just outputs {@code false}.
	 *
	 * @param  requestId The id of the request to find.
	 * @param  f         The predicate to check.
	 * @return           The outcome of the predicate, or false if no lab request with the given id could be
	 *                   found.
	 */
	private boolean withLabRequest(Long requestId, Function<LabRequest, Boolean> f) {
		return lrr.findById(requestId).map(f).orElse(false);
	}

	/**
	 * Checks whether a predicate holds given the lab with the given id. If no such lab could be found, this
	 * function just outputs {@code false}.
	 *
	 * @param  labId The id of the lab to find.
	 * @param  f     The predicate to check.
	 * @return       The outcome of the predicate, or false if no lab with the given id could be found.
	 */
	private boolean withLab(Long labId, Function<QueueSession<?>, Boolean> f) {
		return lr.findById(labId).map(f).orElse(false);
	}

	/**
	 * Checks whether a predicate holds given the session with the given id. If no such session could be
	 * found, this function just outputs {@code false}.
	 *
	 * @param  qSessionId The id of the session to find.
	 * @param  f          The predicate to check.
	 * @return            The outcome of the predicate, or false if no session with the given id could be
	 *                    found.
	 */
	private boolean withQueueSession(Long qSessionId, Function<QueueSession<?>, Boolean> f) {
		return qsr.findById(qSessionId).map(f).orElse(false);
	}

	/**
	 * Checks whether a predicate holds given the module with the given id. If no such module could be found,
	 * this function just outputs {@code false}.
	 *
	 * @param  moduleId The id of the module to find.
	 * @param  f        The predicate to check.
	 * @return          The outcome of the predicate, or false if no module with the given id could be found.
	 */
	private boolean withModule(Long moduleId, Function<ModuleDetailsDTO, Boolean> f) {
		return mCache.get(moduleId).map(f).orElse(false);
	}

	/**
	 * Checks whether a predicate holds given the assignment with the given id. If no such assignment could be
	 * found, this function just outputs {@code false}.
	 *
	 * @param  assignmentId The id of the assignment to find.
	 * @param  f            The predicate to check.
	 * @return              The outcome of the predicate, or false if no assignment with the given id could be
	 *                      found.
	 */
	private boolean withAssignment(Long assignmentId, Function<AssignmentDetailsDTO, Boolean> f) {
		return aCache.get(assignmentId).map(f).orElse(false);
	}

	/**
	 * Checks whether a predicate holds given the session with the given id. If no such session could be
	 * found, this function just outputs {@code false}.
	 *
	 * @param  sessionId The id of the session to find.
	 * @param  f         The predicate to check.
	 * @return           The outcome of the predicate, or false if no session with the given id could be
	 *                   found.
	 */
	private boolean withSession(Long sessionId, Function<SessionDetailsDTO, Boolean> f) {
		return sCache.get(sessionId).map(f).orElse(false);
	}

	/**
	 * Checks whether a predicate holds given the edition with the given id. If no such edition could be
	 * found, this function just outputs {@code false}.
	 *
	 * @param  editionId The id of the edition to find.
	 * @param  f         The predicate to check.
	 * @return           The outcome of the predicate, or false if no edition with the given id could be
	 *                   found.
	 */
	private boolean withEdition(Long editionId, Function<EditionDetailsDTO, Boolean> f) {
		return eCache.get(editionId).map(f).orElse(false);
	}

	/**
	 * Checks whether a predicate holds given the edition collection with the given id. If no such edition
	 * collection could be found, this function just outputs {@code false}.
	 *
	 * @param  editionCollectionId The id of the edition collection to find.
	 * @param  f                   The predicate to check.
	 * @return                     The outcome of the predicate, or false if no edition collection with the
	 *                             given id could be found.
	 */
	private boolean withEditionCollection(Long editionCollectionId,
			Function<EditionCollectionDetailsDTO, Boolean> f) {
		return ecCache.get(editionCollectionId).map(f).orElse(false);
	}

	/**
	 * Checks whether a predicate holds given the list of editions the given lab is in.
	 *
	 * @param  qSession The lab for which to check editions.
	 * @param  f        The predicate to check.
	 * @return          The outcome of the predicate, or false if the lab was not related to any session in
	 *                  Labracore.
	 */
	private boolean withEditions(QueueSession<?> qSession, Function<List<Long>, Boolean> f) {
		return withSession(qSession.getSession(), s -> f.apply(s.getEditions().stream()
				.map(EditionSummaryDTO::getId).collect(Collectors.toList())));
	}

	/**
	 * Checks whether a predicate holds given the student group found by the given id. If no such student
	 * group could be found, this method just returns {@code false}.
	 *
	 * @param  studentGroupId The id of the student group to look up.
	 * @param  f              The predicate to check.
	 * @return                The outcome of the predicat, or false if no student group could be found.
	 */
	private boolean withStudentGroup(Long studentGroupId, Function<StudentGroupDetailsDTO, Boolean> f) {
		return sgCache.get(studentGroupId).map(f).orElse(false);
	}

	/**
	 * Checks whether a predicate holds given a person and the type of their default role.
	 *
	 * @param  personId The id of the person
	 * @param  f        The predicate to check
	 * @return          The outcome of the predicate
	 */
	private boolean withDefaultRole(Long personId, Predicate<DefaultRole> f) {
		var person = pCache.getRequired(personId);
		return f.test(DefaultRole.valueOf(person.getDefaultRole().name()));
	}

	/**
	 * Checks whether a predicate holds given the authenticated person and the type of their role within any
	 * of the given course editions. If no such role could be found, this function outputs {@code false}.
	 *
	 * @param  editionIds The course edition to check the person's role for.
	 * @param  f          The predicate to check.
	 * @return            The outcome of the predicate, or false if nothing could be found.
	 */
	private boolean withAnyRole(List<Long> editionIds,
			Function2<Person, RoleDetailsDTO.TypeEnum, Boolean> f) {
		return withAuthenticatedUser(
				person -> rCache
						.getAndIgnoreMissing(
								editionIds.stream()
										.map(e -> new RoleId().editionId(e).personId(person.getId())))
						.stream().filter(Objects::nonNull)
						.anyMatch(role -> f.apply(person, role.getType())));
	}

	/**
	 * Checks whether a predicate holds given the authenticated person and the type of their role within a
	 * given course edition. If no such role could be found, this function outputs {@code false}.
	 *
	 * @param  editionId The course edition to check the person's role for.
	 * @param  f         The predicate to check.
	 * @return           The outcome of the predicate, or false if nothing could be found.
	 */
	private boolean withRole(Long editionId, Function2<Person, RoleDetailsDTO.TypeEnum, Boolean> f) {
		return withAuthenticatedUser(
				person -> rCache.get(new RoleId().editionId(editionId).personId(person.getId()))
						.map(role -> f.apply(person, role.getType()))
						.orElse(false));
	}

	/**
	 * Checks whether a predicate holds given the role of the given person in the given course edition. If no
	 * such role can be found, this function always outputs {@code false}
	 *
	 * @param  editionId The id of the course edition to check the role of.
	 * @param  personId  The id of the person to check the role of.
	 * @param  f         The predicate to check.
	 * @return           The outcome of the predicate, or false if no role could be found.
	 */
	private boolean withRole(Long editionId, Long personId, Function<RoleDetailsDTO.TypeEnum, Boolean> f) {
		return rCache.get(new RoleId().editionId(editionId).personId(personId))
				.map(role -> f.apply(role.getType()))
				.orElse(false);
	}

	/**
	 * @param  editionId The id of the edition to check for a blocked role.
	 * @return           Whether the currently authenticated person has a blocked role in the given course
	 *                   edition.
	 */
	private boolean hasBlockedRole(Long editionId) {
		return withRole(editionId, (person, type) -> type == BLOCKED);
	}

	/**
	 * @param  editionIds The ids of the editions to check for roles.
	 * @return            Whether the currently authenticated person has a staff role in any of the given
	 *                    course editions.
	 */
	public boolean hasStaffRole(List<Long> editionIds) {
		return withAnyRole(editionIds, (person, role) -> STAFF_ROLES.contains(role));
	}

	/**
	 * @param  editionId The id of the edition to check for roles.
	 * @return           Whether the currently authenticated person has a management role in the given course
	 *                   edition.
	 */
	private boolean hasManagerRole(Long editionId) {
		return withRole(editionId, (person, role) -> MANAGER_ROLES.contains(role));
	}

	/**
	 * @return Whether the authenticated user is a system admin.
	 */
	public boolean isAdmin() {
		return withAuthenticatedUser(person -> person.getDefaultRole() == DefaultRole.ADMIN);
	}

	/**
	 * @return Whether the authenticated user is by default a system admin or a teacher.
	 */
	public boolean isAdminOrTeacher() {
		return withAuthenticatedUser(
				person -> Set.of(DefaultRole.ADMIN, DefaultRole.TEACHER).contains(person.getDefaultRole()));
	}

	public boolean isAuthenticated() {
		return withAuthenticatedUser(person -> person != null);
	}

	/**
	 * @param  editionId The id of the course to enrol to.
	 * @return           Whether the authenticated user can enrol for the course with the given id.
	 */
	public boolean canEnrolForEdition(Long editionId) {
		return !withRole(editionId, (person, type) -> true);
	}

	/**
	 * @param  personId The id of the person to view feedback of.
	 * @return          Whether the authenticated user can view feedback for the person with the given id.
	 */
	public boolean canViewFeedback(Long personId) {
		return isAdmin() ||
				withAuthenticatedUser(person -> personId.equals(person.getId())) ||
				(isAdminOrTeacher() && withDefaultRole(personId, role -> role == DefaultRole.STUDENT));
	}

	/**
	 * @return Whether the authenticated user is allowed to see deanonymised feedback.
	 */
	public boolean canViewDeanonimisedFeedback() {
		return isAdminOrTeacher();
	}

	/**
	 * @return Whether the authenticated user can view their own feedback.
	 */
	public boolean canViewOwnFeedback() {
		return withAuthenticatedUser(person -> true);
	}

	/**
	 * @param  editionId The edition the authenticated user wants to see.
	 * @return           Whether the authenticated user can see the edition with the given id.
	 */
	public boolean canViewEdition(Long editionId) {
		return isAdmin() || withRole(editionId, (person, role) -> role != BLOCKED);
	}

	/**
	 * Determine if a user has manage perms for a edition/shared edition.
	 *
	 * @param  editionList The editions to check for
	 * @return             true iff the person is allowed to modify said editions, false otherwise
	 */
	public boolean canManageInAnyEdition(List<EditionSummaryDTO> editionList) {
		return editionList.stream().anyMatch(edition -> canManageSessions(edition.getId()));
	}

	/**
	 * @param  edition The edition the authenticated user wants to see.
	 * @return         Whether the authenticated user can see the edition.
	 */
	public boolean canViewEdition(EditionDetailsDTO edition) {
		return canViewEdition(edition.getId());
	}

	/**
	 * @param  editionId The edition the authenticated user wants to see the status of.
	 * @return           Whether the authenticated user can see the edition status page.
	 */
	public boolean canViewEditionStatus(Long editionId) {
		return isAdmin() || withRole(editionId, (person, role) -> MANAGER_ROLES.contains(role));
	}

	/**
	 * @param  editionId The edition the authenticated user wants to leave.
	 * @return           Whether the authenticated user can remove themselves from an edition. A user may not
	 *                   leave the course they are in if their role in the course is already linked to a
	 *                   student group, as this means they might have requests, submissions, or even grades in
	 *                   the course that cannot be disassociated with the user. A user that is the only
	 *                   teacher of the edition can't leave the edition.
	 */
	public boolean canLeaveEdition(Long editionId) {
		return withRole(editionId, (person, role) -> (STAFF_ROLES.contains(role) &&
				withEdition(editionId, edition -> {
					return !rds.isTheOnlyTeacherInEdition(person.getId(), edition);
				}))
				||
				(role == STUDENT && withEdition(editionId, edition -> {
					var groups = sgCache.getByPerson(person.getId());
					var modules = edition.getModules().stream()
							.map(ModuleSummaryDTO::getId).collect(Collectors.toSet());

					return groups.stream().noneMatch(sg -> modules.contains(sg.getModule().getId()));
				})));
	}

	/**
	 * @param  qSession The session the authenticated user wants to see.
	 * @return          Whether the authenticated user can see the lab. (Depends on whether they're allowed to
	 *                  see any of the editions that the lab is a part of)
	 */
	public boolean canViewSession(QueueSession<?> qSession) {
		return isAdmin() || withSession(qSession.getSession(),
				s -> s.getEditions().stream().anyMatch(e -> canViewEdition(e.getId())));
	}

	/**
	 * @param  requestId The id of the request the user wants to view.
	 * @return           Whether the authenticated user can see the request with the given id.
	 */
	public boolean canViewRequest(Long requestId) {
		return isAdmin() || withRequest(requestId,
				request -> withAuthenticatedUser(person -> person.getId() != null &&
						person.getId().equals(request.getRequester())) ||
						withEditions(request.getSession(), this::hasStaffRole));
	}

	/**
	 * @param  requestId The id of the request the user wants to view the assistant reason for.
	 * @return           Whether the authenticated user can see the assistant-specific reason for the request
	 *                   with the given id.
	 */
	public boolean canViewRequestAssistantReason(Long requestId) {
		return isAdmin()
				|| withRequest(requestId, request -> withEditions(request.getSession(), this::hasStaffRole));
	}

	/**
	 * @param  requestId The id of the request the user wants to view jitsi room information for.
	 * @return           Whether the authenticated user can see the jitsi room info for the request with the
	 *                   given id.
	 */
	public boolean canViewRequestJitsiRoom(Long requestId) {
		return isAdmin() || withLabRequest(requestId, request -> request.getJitsiRoom() != null &&
				(withEditions(request.getSession(), this::hasStaffRole) ||
						withAuthenticatedUser(
								person -> Objects.equals(request.getRequester(), person.getId()))));
	}

	/**
	 * @return Whether the authenticated user can see the request table view.
	 */
	public boolean canViewRequests() {
		return isAdminOrTeacher()
				|| withAuthenticatedUserMono(person -> aApi.hasStaffRoleInAnyEdition(person.getId()));
	}

	/**
	 * @param  editionId The id of the edition the user wants to manage.
	 * @return           Whether the authenticated user can change some values within the settings of the
	 *                   edition.
	 */
	public boolean canManageEdition(Long editionId) {
		return isAdmin() || withRole(editionId, (person, role) -> TEACHER == role);
	}

	/**
	 * @param  qSession The session that the user wants to manage.
	 * @return          Whether the authenticated user can change properties of the given session.
	 */
	public boolean canManageSession(QueueSession<?> qSession) {
		return isAdmin() || withEditions(qSession,
				editionIds -> withAnyRole(editionIds, (p, t) -> MANAGER_ROLES.contains(t)));
	}

	/**
	 * @param  moduleId The id of the module that the user wants to manage.
	 * @return          Whether the authenticated user can change properties of the given module and add
	 *                  assignments/groups.
	 */
	public boolean canManageModule(Long moduleId) {
		return isAdmin() || withModule(moduleId, module -> canManageModules(module.getEdition().getId()));
	}

	/**
	 * @param  editionId The id of the edition the user wants to manage modules for.
	 * @return           Whether the authenticated user can add/remove/change any modules within the edition
	 *                   with the given id.
	 */
	public boolean canManageModules(Long editionId) {
		return isAdmin() || hasManagerRole(editionId);
	}

	/**
	 * @param  assignmentId The is of the assignment that user wants to manage.
	 * @return              Whether the authenticated user can change properties of the given assignment.
	 */
	public boolean canManageAssignment(Long assignmentId) {
		return isAdmin() || withAssignment(assignmentId,
				assignment -> canManageModule(assignment.getModule().getId()));
	}

	/**
	 * Checks whether the user can manage the questions of an edition.
	 *
	 * @param  editionId The id of an edition
	 * @return           Whether the authenticated user can manage the questions of an edition
	 */
	public boolean canManageQuestions(Long editionId) {
		return isAdmin() || withRole(editionId, (person, role) -> TEACHER == role || HEAD_TA == role);
	}

	/**
	 * @param  editionId The id of the edition the user wants to manage teachers for.
	 * @return           Whether the authenticated user can add and remove teachers to and from the edition
	 *                   with the given id.
	 */
	public boolean canManageTeachers(Long editionId) {
		return isAdmin() || withRole(editionId, (person, role) -> TEACHER == role);
	}

	/**
	 * @param  editionId The id of the edtiion the user wants to manage participants for.
	 * @return           Whether the authenticated user can add/remove/change any roles within the edition
	 *                   with the given id.
	 */
	public boolean canManageParticipants(Long editionId) {
		return isAdmin() || hasManagerRole(editionId);
	}

	/**
	 * @param  editionId The id of the edition the user wants to manage the participant for.
	 * @param  personId  The specific participant the user wants to change a role or remove a role for.
	 * @return           Whether the authenticated user can add/remove/change the role for the person with the
	 *                   given id within the edition with the given id.
	 */
	public boolean canManageParticipant(Long editionId, Long personId) {
		return isAdmin() || withRole(editionId, (person, role) -> TEACHER == role || (HEAD_TA == role
				&& withRole(editionId, personId, NON_MANAGER_ROLES::contains)));
	}

	/**
	 * @param  editionId The id of the edition the user wants to manage assignments for.
	 * @return           Whether the authenticated user can add/remove/change assignments within the edition
	 *                   with the given id.
	 */
	public boolean canManageAssignments(Long editionId) {
		return isAdmin() || hasManagerRole(editionId);
	}

	/**
	 * @param  editionId The id of the edition the user wants to manage sessions for.
	 * @return           Whether the authenticated user can add/remove/change sessions within the edition with
	 *                   the given id.
	 */
	public boolean canManageSessions(Long editionId) {
		return isAdmin() || hasManagerRole(editionId);
	}

	/**
	 * @param  editionCollectionId The id of the edition collection the user wants to manage sessions for.
	 * @return                     Whether the authenticated user can add/remove/change sessions within the
	 *                             edition collection with the given id.
	 */
	public boolean canManageSharedSessions(Long editionCollectionId) {
		return isAdmin() || withEditionCollection(editionCollectionId,
				ec -> withAnyRole(
						ec.getEditions().stream().map(EditionSummaryDTO::getId).collect(Collectors.toList()),
						(person, role) -> MANAGER_ROLES.contains(role)));
	}

	/**
	 * @param  timeSlot The time slot the user wants to take a next request from.
	 * @return          Whether the authenticated user can take a next request from the given time slot.
	 */
	public boolean canTakeFromTimeSlot(ClosableTimeSlot timeSlot) {
		return isAdmin() || (timeSlot.getActive() && canTakeRequest(timeSlot.getLab().getId()));
	}

	/**
	 * @param  timeSlot The time slot the user wants to manage.
	 * @return          Whether the authenticated user can manage the given time slot.
	 */
	public boolean canManageTimeSlot(TimeSlot timeSlot) {
		return isAdmin() || canManageSession(timeSlot.getLab());
	}

	/**
	 * @param  timeSlot The time slot the user wants to close.
	 * @return          Whether the authenticated user can close the given time slot manually.
	 */
	public boolean canCloseTimeSlot(ClosableTimeSlot timeSlot) {
		return isAdmin() || (timeSlot.getActive() && canManageSession(timeSlot.getLab()));
	}

	/**
	 * @param  requestId The id of the request the user wants to finish.
	 * @return           Whether the authenticated user can finish the request with the given id.
	 */
	public boolean canFinishRequest(Long requestId) {
		return isAdmin() || withRequest(requestId,
				request -> withEditions(request.getSession(),
						editions -> withAnyRole(editions, (person, role) -> {
							switch (role) {
								case TEACHER:
									return !request.getEventInfo().getStatus().isHandled()
											&& request.getEventInfo().getStatus() != RequestStatus.REVOKED;
								case HEAD_TA:
								case TA:
									return Objects
											.equals(request.getEventInfo().getAssignedTo(), person.getId());
								default:
									return false;
							}
						})));
	}

	/**
	 * @param  requestId The id of the request that the authenticated user wants to give feedback on.
	 * @return           Whether the authenticated user can add feedback to the request with the given id.
	 */
	public boolean canGiveFeedback(Long requestId) {
		return withRequest(requestId, request -> request.getSession().getType() != QueueSessionType.EXAM &&
				withStudentGroup(request.getStudentGroup(),
						sg -> withAuthenticatedUser(person -> sg.getMembers().stream()
								.anyMatch(role -> role.getType() == RolePersonLayer1DTO.TypeEnum.STUDENT &&
										Objects.equals(role.getPerson().getId(), person.getId())))));
	}

	/**
	 * @param  requestId   The id of the request the user wants to give feedback on.
	 * @param  assistantId The id of the assistant the user wants to give feedback on.
	 * @return             Whether the authenticated user can add feedback for the assistant on the given
	 *                     request.
	 */
	public boolean canGiveFeedback(Long requestId, Long assistantId) {
		return canGiveFeedback(requestId) && withRequest(requestId,
				request -> request.getEventInfo().involvedAssistants().contains(assistantId));
	}

	/**
	 * @param  requestId The id of the request the user wants to edit.
	 * @return           Whether the authenticated user can update the request with the given id.
	 */
	public boolean canUpdateRequest(Long requestId) {
		return isAdmin() || withRequest(requestId, request -> withAuthenticatedUser(
				person -> Objects.equals(request.getRequester(), person.getId())));
	}

	/**
	 * @param  labId The id of the lab the user wants to take a request for.
	 * @return       Whether the authenticated user can take a request within the given lab.
	 */
	public boolean canTakeRequest(Long labId) {
		return isAdmin() || withLab(labId, lab -> withEditions(lab, this::hasStaffRole));
	}

	/**
	 * @param  qSessionId The id of the session for which the user wants to see requests.
	 * @return            Whether the authenticated user can see the request list for this lab.
	 */
	public boolean canViewSessionRequests(Long qSessionId) {
		return isAdmin()
				|| withQueueSession(qSessionId, qSession -> withEditions(qSession, this::hasStaffRole));
	}

	/**
	 * @param  qSessionId The id of the session the user would like to enqueue/enrol in.
	 * @return            Whether the authenticated user can place a new request on the Queue for the given
	 *                    session because they are in the course the session is in.
	 */
	public boolean canEnqueueSelf(Long qSessionId) {
		return withQueueSession(qSessionId,
				qSession -> labStatusService.getStatus(qSession).isOpenToEnqueueing() &&
						withEditions(qSession,
								editions -> withAnyRole(editions, (person, role) -> role == STUDENT)));
	}

	/**
	 * @param  qSessionId The session the user wants to enqueue into.
	 * @return            Whether the authenticated user can place a new request on the Queue for the session
	 *                    with the given id.
	 */
	public boolean canEnqueueSelfNow(Long qSessionId) {
		return withQueueSession(qSessionId,
				lab -> labStatusService.getStatus(lab).isOpenToEnqueueing() &&
						withEditions(lab,
								editions -> withAnyRole(editions, (person, role) -> role == STUDENT &&
										!lab.hasOpenRequestForPerson(person.getId()) &&
										rs.getOpenRequestForGroupOfPerson(person.getId(), lab)
												.isEmpty()
										&&
										labRequestConstraintService.canCreateRequest(lab.getConstraints(),
												person.getId()))));
	}

	/**
	 * @param  request The request that the user wants to revoke.
	 * @return         Whether the authenticated user can revoke the given request.
	 */
	public boolean canRevokeRequest(Request<?> request) {
		if (!request.isRevokable())
			return false;

		return isAdmin() || withEdition(requestViewDTOService.convert(request).getEdition().getId(),
				edition -> hasManagerRole(edition.getId()))
				|| withAuthenticatedUser(
						person -> Objects.equals(request.getRequester(), person.getId()) &&
								withEditions(request.getSession(),
										editions -> withAnyRole(editions, (_p, role) -> role == STUDENT)));
	}

	/**
	 * @param  requestId The request the user wants to pick.
	 * @return           Whether the autenticated user can pick the request, when he/she is a manager of the
	 *                   course and the request is not yet assigned to anybody.
	 */
	public boolean canPickRequest(Long requestId) {
		return isAdmin() || withRequest(requestId,
				request -> withEdition(requestViewDTOService.convert(request).getEdition().getId(),
						edition -> withRole(edition.getId(),
								(person, role) -> MANAGER_ROLES.contains(role)))
						&& (request.getEventInfo().getStatus() == RequestStatus.PENDING
								|| request.getEventInfo().getStatus() == RequestStatus.FORWARDED));
	}

	/**
	 * @return Whether or not the person is a manager of the course and can create a new edition.
	 */
	public boolean canCreateEdition() {
		return isAdminOrTeacher();
	}

	/**
	 * @param  courseId The id of the course to create an edition for.
	 * @return          Whether or not the person is a manager of the course and can create a new edition.
	 */
	public boolean canCreateEdition(long courseId) {
		return withAuthenticatedUser(
				person -> cApi.getCourseById(courseId).block().getManagers().stream()
						.anyMatch(m -> m.getId().equals(person.getId()))
						|| isAdmin());
	}

	/**
	 * Checks if the authenticated user can join a given group.
	 *
	 * @param  groupId The id of the group to join
	 * @return         True iff the user can join the group
	 */
	public boolean canJoinGroup(long groupId) {
		StudentGroupDetailsDTO group = sgCache.getRequired(groupId);
		ModuleDetailsDTO module = mCache.getRequired(group.getModule().getId());
		return withAuthenticatedUser(person -> withRole(module.getEdition().getId(), person.getId(),
				role -> group.getMembers().size() < group.getCapacity()));
	}
}
