/*
 * Queue - A Queueing system that can be used to handle labs in higher education
 * Copyright (C) 2016-2021  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.PersonDetailsDTO.DefaultRoleEnum.ADMIN;
import static nl.tudelft.labracore.api.dto.PersonDetailsDTO.DefaultRoleEnum.TEACHER;
import static nl.tudelft.labracore.api.dto.RoleEditionDetailsDTO.TypeEnum.TEACHER_RO;

import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

import nl.tudelft.labracore.api.EditionControllerApi;
import nl.tudelft.labracore.api.PersonControllerApi;
import nl.tudelft.labracore.api.SessionControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
import nl.tudelft.labracore.lib.security.user.DefaultRole;
import nl.tudelft.labracore.lib.security.user.Person;
import nl.tudelft.librador.dto.view.View;
import nl.tudelft.queue.PageUtil;
import nl.tudelft.queue.cache.EditionCacheManager;
import nl.tudelft.queue.cache.EditionCollectionCacheManager;
import nl.tudelft.queue.cache.PersonCacheManager;
import nl.tudelft.queue.cache.SessionCacheManager;
import nl.tudelft.queue.dto.view.FeedbackViewDTO;
import nl.tudelft.queue.dto.view.QueueEditionDetailsDTO;
import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO;
import nl.tudelft.queue.model.Feedback;
import nl.tudelft.queue.repository.FeedbackRepository;
import nl.tudelft.queue.repository.LabRepository;
import nl.tudelft.queue.service.EditionService;
import nl.tudelft.queue.service.FeedbackService;
import nl.tudelft.queue.service.LabService;
import nl.tudelft.queue.service.PermissionService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;

@Controller
public class HomeController {

	@Autowired
	private FeedbackRepository fr;

	@Autowired
	private LabRepository lr;

	@Autowired
	private LabService ls;

	@Autowired
	private FeedbackService fs;

	@Autowired
	private EditionControllerApi eApi;

	@Autowired
	private PersonControllerApi pApi;

	@Autowired
	private EditionCacheManager eCache;

	@Autowired
	private EditionCollectionCacheManager ecCache;

	@Autowired
	private PersonCacheManager pCache;

	@Autowired
	private SessionCacheManager sCache;

	@Autowired
	private SessionControllerApi sApi;

	@Autowired
	private EditionService es;

	@Autowired
	private PermissionService ps;

	/**
	 * Sets a model attribute statically within every Thymeleaf resolution. This model attribute is to
	 * indicate the main tab that Home pages are in.
	 *
	 * @return The name of the tab that these pages are on ("home").
	 */
	@ModelAttribute("page")
	public String page() {
		return "main";
	}

	/**
	 * @return The path for the privacy statement.
	 */
	@GetMapping("/privacy")
	public String privacyStatement() {
		return "home/privacy";
	}

	/**
	 * @return The path for the about page.
	 */
	@GetMapping("/about")
	public String about() {
		return "home/about";
	}

	/**
	 * Maps the index/root url to a page. When the user is logged in correctly with an
	 * AnonymousAuthenticationToken, the user is directed to a dashboard view of the Queue. When the user is
	 * not correctly logged in, they are redirected to the introductory page of the Queue.
	 *
	 * @param  person The person currently authenticated for which a dashboard might be shown.
	 * @param  model  The model to be filled out when the user has editions and/or labs to show on their
	 *                dashboard.
	 * @return        The path to the corresponding Thymeleaf template.
	 */
	@GetMapping("/")
	public String index(@AuthenticatedPerson(required = false) Person person, Model model) {
		var now = LocalDateTime.now();

		if (person != null) {
			PersonDetailsDTO pd = pApi.getPersonById(person.getId()).block();

			var roles = pApi
					.getRolesForPerson(Objects.requireNonNull(pd).getId())
					.sort(Comparator.comparing((RoleEditionDetailsDTO r) -> r.getEdition().getEndDate())
							.reversed())
					.collectList().block();
			var activeRoles = roles.stream()
					.filter(role -> !role.getEdition().getIsArchived() &&
							role.getEdition().getStartDate().isBefore(now) &&
							now.isBefore(role.getEdition().getEndDate()))
					.collect(Collectors.toList());
			var finishedRoles = roles.stream()
					.filter(role -> !role.getEdition().getIsArchived() &&
							role.getEdition().getEndDate().isBefore(now))
					.sorted(Comparator.comparing(RoleEditionDetailsDTO::getType)
							.thenComparing((RoleEditionDetailsDTO r) -> r.getEdition().getEndDate())
							.reversed())
					.collect(Collectors.toList());
			var upcomingRoles = roles.stream()
					.filter(role -> role.getEdition().getStartDate().isAfter(now))
					.sorted(Comparator.comparing(RoleEditionDetailsDTO::getType)
							.thenComparing((RoleEditionDetailsDTO r) -> r.getEdition().getStartDate())
							.reversed())
					.collect(Collectors.toList());
			var archivedRoles = roles.stream()
					.filter(role -> role.getEdition().getIsArchived() &&
							Set.of(TEACHER_RO).contains(role.getType()))
					.collect(Collectors.toList());

			var editions = eCache.getAndIgnoreMissing(roles.stream().map(r -> r.getEdition().getId()))
					.stream()
					.collect(Collectors.toMap(EditionDetailsDTO::getId,
							e -> es.queueEditionDTO(e, QueueEditionDetailsDTO.class)));

			Set<SessionDetailsDTO> sessions = new HashSet<>(sCache.getAndHandleAll(editions.values().stream()
					.flatMap(e -> e.getSessions().stream())
					.filter(s -> s.getEndTime().isAfter(now.minusMonths(1)))
					.map(SessionSummaryDTO::getId), missingIds -> ls.deleteSessionsByIds()));

			var sharedEditions = editions.values().stream().map(EditionDetailsDTO::getEditionCollections)
					.flatMap(List::stream).map(e -> ecCache.getRequired(e.getId()))
					.collect(Collectors.toSet());

			var calendarEntries = ls.convertToCalendarEntries(
					lr.findAllBySessions(
							sessions.stream().map(SessionDetailsDTO::getId).collect(Collectors.toList())));

			sessions = sessions.stream().filter(s -> s.getEndTime().isAfter(now)).collect(Collectors.toSet());
			var labs = sessions.stream().collect(Collectors.toMap(SessionDetailsDTO::getId,
					s -> es.sortLabs(View.convert(lr.findAllBySessions(List.of(s.getId())),
							QueueSessionSummaryDTO.class)).stream()
							.filter(qs -> ps.canEnqueueSelfNow(qs.getId()) || qs.getSlot().open())
							.collect(Collectors.toList())));

			var sharedLabs = sharedEditions.stream().collect(Collectors.toMap(
					EditionCollectionDetailsDTO::getId,
					s -> es.sortLabs(View.convert(new ArrayList<>(lr.findAllBySessions(
							eCache.getAndIgnoreMissing(
									s.getEditions().stream().map(EditionSummaryDTO::getId).toList())
									.stream().map(EditionDetailsDTO::getSessions).flatMap(List::stream)
									.map(SessionSummaryDTO::getId).collect(Collectors.toList()))),
							QueueSessionSummaryDTO.class)).stream()
							.filter(QueueSessionSummaryDTO::getIsShared)
							.filter(qs -> ps.canEnqueueSelf(qs.getId()) || qs.getSlot().open())
							.collect(Collectors.toList())));

			if (pd.getDefaultRole() == ADMIN || pd.getDefaultRole() == TEACHER) {
				model.addAttribute("allEditions", eCache.getAndIgnoreMissing(
						Objects.requireNonNullElse(
								eApi.getAllEditionsActiveDuringPeriod(
										LocalDateTime.now(), LocalDateTime.now().plusYears(1))
										.map(EditionSummaryDTO::getId)
										.collectList().block(),
								List.of())));
			}

			model.addAttribute("editions", editions);
			model.addAttribute("sharedEditions", sharedEditions);
			model.addAttribute("sharedLabs", sharedLabs);
			model.addAttribute("user", pd);
			model.addAttribute("labs", labs);
			model.addAttribute("activeRoles", activeRoles);
			model.addAttribute("finishedRoles", finishedRoles);
			model.addAttribute("archivedRoles", archivedRoles);
			model.addAttribute("upcomingRoles", upcomingRoles);
			model.addAttribute("sessions", calendarEntries);

			getStatusSharedEditions(model, sharedEditions, activeRoles, upcomingRoles, archivedRoles,
					finishedRoles);

			model.addAttribute("page", "my-courses");

			return "home/dashboard";
		}
		return "home/index";
	}

	/**
	 * Maps the own feedback url to a page. The Feedback page displays feedback for the currently
	 * authenticated user.
	 *
	 * @param  person   The person that is currently authenticated.
	 * @param  model    The model to be filled out for Thymeleaf resolution.
	 * @param  pageable The Pageable object representing the current page state.
	 * @return          The feedback page location to be filled in by Thymeleaf.
	 */
	@GetMapping("/feedback")
	@PreAuthorize("@permissionService.canViewOwnFeedback()")
	public String ownFeedback(@AuthenticatedPerson Person person,
			Model model, Pageable pageable) {
		fillInFeedbackModel(person.getId(), person, model, pageable, false);

		return "home/feedback";
	}

	/**
	 * Maps the feedback url to a page. The Feedback page displays feedback for a user with the given ID. This
	 * endpoint is also used by TAM.
	 *
	 * @param  person   The person that is currently authenticated.
	 * @param  id       The id of the person for which feedback will be shown.
	 * @param  model    The model to be filled out for Thymeleaf resolution.
	 * @param  pageable The Pageable object representing the current page state.
	 * @return          The feedback page location to be filled in by Thymeleaf.
	 */
	@GetMapping("/feedback/{id}")
	@PreAuthorize("@permissionService.canViewFeedback(#id)")
	public String feedback(@AuthenticatedPerson Person person, @PathVariable("id") Long id,
			Model model, Pageable pageable) {
		fillInFeedbackModel(id, person, model, pageable, false);

		return "home/feedback";
	}

	@GetMapping("/feedback/{id}/manager")
	@PreAuthorize("@permissionService.canViewFeedback(#id)")
	public String feedbackManager(@AuthenticatedPerson Person person, @PathVariable("id") Long id,
			Model model, Pageable pageable) {

		fillInFeedbackModel(id, person, model, pageable, true);

		return "home/feedback";
	}

	/**
	 * Fills in the model for a page where feedback is shown to the user.
	 *
	 *
	 * @param  assistantId             The id of the user to find assistant for (this could be the current
	 *                                 user).
	 * @param  person                  The person that is currently authenticated.
	 * @param  model                   The model that is to be filled.
	 * @param  pageable                The pageable containing information on how much feedback needs to be
	 *                                 shown.
	 * @param  restrictToCourseManager Used to restrict feedback to the courses a manager teaches.
	 * @throws AccessDeniedException   In the case that a teacher purposefully adds a teacher as a TA to view
	 *                                 their feedback.
	 */
	private void fillInFeedbackModel(Long assistantId, Person person, Model model, Pageable pageable,
			Boolean restrictToCourseManager) {
		var assistant = pCache.getRequired(assistantId);

		if (assistant.getDefaultRole() == PersonSummaryDTO.DefaultRoleEnum.TEACHER
				&& person.getDefaultRole() != DefaultRole.ADMIN) {
			throw new AccessDeniedException(
					"Teachers are not permitted to view the feedback of other teachers.");
		}

		Page<Feedback> feedback = assistantId.equals(person.getId())
				? fr.findByAssistantAnonymised(assistantId, pageable)
				: restrictToCourseManager
						? fs.filterFeedbackForManagerCourses(fr.findByAssistant(assistantId), pageable)
						: PageUtil.toPage(pageable, fr.findByAssistant(assistantId));

		model.addAttribute("assistant", assistant);
		model.addAttribute("feedback",
				feedback.map(fb -> View.convert(fb, FeedbackViewDTO.class)));
		model.addAttribute("stars", fs.countRatings(assistantId));
	}

	/**
	 * Fills in the model for displaying shared editions.
	 *
	 * @param model          The model to be filled in .
	 * @param sharedEditions The entire shared editions.
	 * @param active         The active editions.
	 * @param upcoming       The upcoming editions.
	 * @param archived       The archived editions.
	 * @param finished       The finished editions.
	 */
	private void getStatusSharedEditions(Model model, Set<EditionCollectionDetailsDTO> sharedEditions,
			List<RoleEditionDetailsDTO> active, List<RoleEditionDetailsDTO> upcoming,
			List<RoleEditionDetailsDTO> archived, List<RoleEditionDetailsDTO> finished) {
		var archivedSharedEditions = sharedEditions.stream()
				.filter(s -> s.getEditions().stream().allMatch(EditionSummaryDTO::getIsArchived))
				.collect(Collectors.toSet());

		var activeSharedEditions = sharedEditions.stream()
				.filter(s -> s.getEditions().stream().anyMatch(e -> active.stream()
						.map(RoleEditionDetailsDTO::getEdition).anyMatch(ac -> ac.equals(e))))
				.collect(Collectors.toSet());

		var upcomingSharedEditions = sharedEditions.stream().filter(s -> {
			var editions = s.getEditions();
			var sharedEditionsList = archived.stream().map(RoleEditionDetailsDTO::getEdition)
					.collect(Collectors.toList());
			boolean anyUpcoming = upcoming.stream().map(RoleEditionDetailsDTO::getEdition)
					.anyMatch(editions::contains);
			boolean noUpcomingIsArchived = upcoming.stream().map(RoleEditionDetailsDTO::getEdition)
					.noneMatch(sharedEditionsList::contains);
			boolean noneActive = active.stream().map(RoleEditionDetailsDTO::getEdition)
					.noneMatch(editions::contains);
			return anyUpcoming && noUpcomingIsArchived && noneActive;
		}).collect(Collectors.toSet());

		var finishedSharedEditions = sharedEditions.stream().filter(s -> {
			var editions = s.getEditions();
			boolean anyFinished = finished.stream().map(RoleEditionDetailsDTO::getEdition)
					.anyMatch(editions::contains);
			boolean noneUpcoming = upcoming.stream().map(RoleEditionDetailsDTO::getEdition)
					.noneMatch(editions::contains);
			boolean noneActive = active.stream().map(RoleEditionDetailsDTO::getEdition)
					.noneMatch(editions::contains);
			return anyFinished && noneUpcoming && noneActive;
		}).collect(Collectors.toSet());

		model.addAttribute("archivedSharedEditions", archivedSharedEditions);
		model.addAttribute("activeSharedEditions", activeSharedEditions);
		model.addAttribute("upcomingSharedEditions", upcomingSharedEditions);
		model.addAttribute("finishedSharedEditions", finishedSharedEditions);
	}

}
