/*
 * Queue - A Queueing system that can be used to handle labs in higher education
 * Copyright (C) 2016-2024  Delft University of Technology
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package nl.tudelft.queue.controller;

import static nl.tudelft.labracore.api.dto.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 org.springframework.beans.factory.annotation.Autowired;
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 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.Person;
import nl.tudelft.librador.dto.DTOConverter;
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.QueueEditionDetailsDTO;
import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO;
import nl.tudelft.queue.repository.FeedbackRepository;
import nl.tudelft.queue.repository.LabRepository;
import nl.tudelft.queue.repository.LabRequestRepository;
import nl.tudelft.queue.repository.QueueSessionRepository;
import nl.tudelft.queue.service.*;
import nl.tudelft.queue.service.dto.QueueSessionViewDTOService;

@Controller
public class HomeController {

	@Autowired
	private FeedbackRepository fr;

	@Autowired
	private QueueSessionRepository qsr;

	@Autowired
	private LabRepository lr;

	@Autowired
	private LabRequestRepository lrr;

	@Autowired
	private EditionStatusService ess;

	@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;

	@Autowired
	private SessionService sessionService;

	@Autowired
	private QueueSessionViewDTOService queueSessionViewDTOService;

	@Autowired
	private DTOConverter converter;

	/**
	 * 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";
	}

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

	/**
	 * 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<>(
					sessionService.getCoreSessions(editions.values().stream()
							.flatMap(e -> e.getSessions().stream())
							.filter(s -> s.getEndTime().isAfter(now.minusMonths(1)))
							.map(SessionSummaryDTO::getId).toList()));

			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(qsr.findAllBySessions(List.of(s.getId())).stream()
							.map(queueSessionViewDTOService::convertToSummary)
							.filter(qs -> ps.canEnqueueSelfNow(qs.getId()) || qs.getSlot().open())
							.collect(Collectors.toList()))));

			var sharedLabs = sharedEditions.stream().collect(Collectors.toMap(
					EditionCollectionDetailsDTO::getId,
					s -> es.sortLabs(new ArrayList<>(lr.findAllBySessions(
							eCache.getAndIgnoreMissing(
									s.getEditions().stream().map(EditionSummaryDTO::getId).toList())
									.stream()
									.map(EditionDetailsDTO::getSessions)
									.flatMap(List::stream)
									.map(SessionSummaryDTO::getId)
									.toList())
							.stream().map(queueSessionViewDTOService::convertToSummary).toList())
							.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";
	}

	/**
	 * 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);
	}

}
