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

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

import lombok.AllArgsConstructor;
import nl.tudelft.labracore.api.RoleControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.labracore.lib.security.user.Person;
import nl.tudelft.queue.cache.EditionCacheManager;
import nl.tudelft.queue.cache.EditionRolesCacheManager;
import nl.tudelft.queue.model.QueueSession;

@Service
@AllArgsConstructor
public class RoleDTOService {

	private EditionRolesCacheManager erCache;

	private RoleControllerApi rApi;

	private EditionCacheManager eCache;

	@Lazy
	private SessionService sessionService;

	public List<String> names(List<PersonSummaryDTO> people) {
		return people.stream().map(PersonSummaryDTO::getDisplayName).collect(Collectors.toList());
	}

	/**
	 * Gets the names of staff in a lab.
	 *
	 * @param  qSession The session to consider.
	 * @param  editions Filter by editions, useful for shared labs, leave empty for no filter.
	 * @return          A mapping of ids to names of staff
	 */
	public Map<Long, String> staffNames(QueueSession<?> qSession, Set<Long> editions) {
		return erCache
				.getAndIgnoreMissing(sessionService.getSessionDTOFromSession(qSession)
						.getEditions().stream().map(EditionSummaryDTO::getId)
						.filter(eId -> editions.isEmpty() || editions.contains(eId)))
				.stream()
				.flatMap(e -> e.getRoles().stream())
				.filter(this::isStaff)
				.map(RolePersonDetailsDTO::getPerson)
				.distinct()
				.collect(Collectors.toMap(PersonSummaryDTO::getId, PersonSummaryDTO::getDisplayName));
	}

	public List<String> roleNames(List<RolePersonDetailsDTO> roles,
			Set<RolePersonDetailsDTO.TypeEnum> types) {
		return roles.stream()
				.filter(role -> types.contains(role.getType()))
				.map(role -> Objects.requireNonNull(role.getPerson()).getDisplayName())
				.collect(Collectors.toList());
	}

	public List<String> roleNames(EditionDetailsDTO eDto, Set<RolePersonDetailsDTO.TypeEnum> types) {
		return roleNames(erCache.getRequired(eDto.getId()).getRoles(), types);
	}

	public List<PersonSummaryDTO> roles(List<RolePersonDetailsDTO> roles,
			Set<RolePersonDetailsDTO.TypeEnum> types) {
		return roles.stream()
				.filter(role -> types.contains(role.getType()))
				.map(RolePersonDetailsDTO::getPerson)
				.collect(Collectors.toList());
	}

	public List<PersonSummaryDTO> rolesL1(List<RolePersonLayer1DTO> roles,
			Set<RolePersonLayer1DTO.TypeEnum> types) {
		return roles.stream()
				.filter(role -> types.contains(role.getType()))
				.map(RolePersonLayer1DTO::getPerson)
				.collect(Collectors.toList());
	}

	public List<PersonSummaryDTO> roles(EditionDetailsDTO eDto,
			Set<RolePersonDetailsDTO.TypeEnum> types) {
		return roles(erCache.getRequired(eDto.getId()).getRoles(), types);
	}

	public List<PersonSummaryDTO> roles(EditionSummaryDTO eDto, Set<RolePersonDetailsDTO.TypeEnum> types) {
		return roles(eCache.getRequired(eDto.getId()), types);
	}

	/**
	 * Gets the display name of the role type.
	 *
	 * @param  type The type of a role as a string.
	 * @return      The user-friendly representation of the role type.
	 */
	public String typeDisplayName(String type) {
		return switch (type) {
			case "STUDENT" -> "Student";
			case "TA" -> "TA";
			case "HEAD_TA" -> "Head TA";
			case "TEACHER" -> "Teacher";
			case "TEACHER_RO" -> "Read-Only Teacher";
			case "ADMIN" -> "Admin";
			case "BLOCKED" -> "Blocked";
			default -> "No role";
		};
	}

	public List<String> assistantNames(EditionDetailsDTO eDto) {
		return roleNames(eDto, Set.of(TA));
	}

	public List<String> headTANames(EditionDetailsDTO eDto) {
		return roleNames(eDto, Set.of(HEAD_TA));
	}

	public List<String> teacherNames(EditionDetailsDTO eDto) {
		return roleNames(eDto, Set.of(TEACHER, TEACHER_RO));
	}

	public List<PersonSummaryDTO> students(EditionDetailsDTO eDto) {
		return roles(eDto, Set.of(STUDENT));
	}

	public List<PersonSummaryDTO> students(List<RolePersonDetailsDTO> roles) {
		return roles(roles, Set.of(STUDENT));
	}

	public List<PersonSummaryDTO> studentsL1(List<RolePersonLayer1DTO> roles) {
		return rolesL1(roles, Set.of(RolePersonLayer1DTO.TypeEnum.STUDENT));
	}

	public List<PersonSummaryDTO> assistants(EditionDetailsDTO eDto) {
		return roles(eDto, Set.of(TA));
	}

	public List<PersonSummaryDTO> staff(EditionSummaryDTO eDto) {
		return roles(eDto, Set.of(TA, HEAD_TA, TEACHER, TEACHER_RO));
	}

	public List<PersonSummaryDTO> headTAs(EditionDetailsDTO eDto) {
		return roles(eDto, Set.of(HEAD_TA));
	}

	public List<PersonSummaryDTO> teachers(EditionDetailsDTO eDto) {
		return roles(eDto, Set.of(TEACHER, TEACHER_RO));
	}

	public List<RoleDetailsDTO> rolesForPersonInEdition(EditionSummaryDTO eDto, Person person) {
		return rApi.getRolesById(Set.of(eDto.getId()), Set.of(person.getId())).collectList().block();
	}

	public boolean isStaff(RoleDetailsDTO role) {
		var type = role.getType();
		return Set.of(RoleDetailsDTO.TypeEnum.TA, RoleDetailsDTO.TypeEnum.HEAD_TA,
				RoleDetailsDTO.TypeEnum.TEACHER, RoleDetailsDTO.TypeEnum.TEACHER_RO).contains(type);
	}

	public boolean isStaff(RolePersonDetailsDTO role) {
		return Set.of(TA, HEAD_TA, TEACHER, TEACHER_RO).contains(role.getType());
	}

	/**
	 * Checks if the person has a teacher role in the given addition
	 *
	 * @param  personId the id of the person whose role is being checked
	 * @param  eDto     the edition the roles will be checked from
	 * @return          whether the person is a part of the edition or not
	 */
	public boolean isTeacherInEdition(Long personId, EditionDetailsDTO eDto) {
		return teachers(eDto).stream().map(PersonSummaryDTO::getId).toList().contains(personId);
	}

	/**
	 * Checks if the teacher is the only teacher in the given edition
	 *
	 * @param  personId the id of person whose role is checked to be teacher
	 * @param  eDto     the edition to check the role from
	 * @return          whether the person is the only teacher in the edition
	 */
	public boolean isTheOnlyTeacherInEdition(Long personId, EditionDetailsDTO eDto) {
		var teacherIds = teachers(eDto).stream().map(PersonSummaryDTO::getId).toList();
		return teacherIds.contains(personId) && teacherIds.size() == 1;

	}
}
