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

import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.*;

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

import javax.servlet.http.HttpSession;
import javax.transaction.Transactional;

import nl.tudelft.labracore.api.AssignmentControllerApi;
import nl.tudelft.labracore.api.EditionControllerApi;
import nl.tudelft.labracore.api.SessionControllerApi;
import nl.tudelft.labracore.api.dto.*;
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.*;
import nl.tudelft.queue.dto.util.RequestTableFilterDTO;
import nl.tudelft.queue.dto.view.QueueSessionSummaryDTO;
import nl.tudelft.queue.dto.view.RequestViewDTO;
import nl.tudelft.queue.model.QueueSession;
import nl.tudelft.queue.model.Request;
import nl.tudelft.queue.model.enums.RequestStatus;
import nl.tudelft.queue.model.labs.AbstractSlottedLab;
import nl.tudelft.queue.model.labs.Lab;
import nl.tudelft.queue.repository.LabRepository;
import nl.tudelft.queue.repository.LabRequestRepository;
import nl.tudelft.queue.repository.ProfileRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;

@Service
public class RequestTableService {
	private static final Integer MAX_GRACE_PERIOD = 180;

	@Autowired
	private HttpSession session;

	@Autowired
	private LabRepository lr;

	@Autowired
	private LabRequestRepository rr;

	@Autowired
	private ProfileRepository pr;

	@Autowired
	private EditionControllerApi eApi;

	@Autowired
	private SessionControllerApi sApi;

	@Autowired
	private AssignmentControllerApi aApi;
	@Autowired
	private AssignmentCacheManager aCache;

	@Autowired
	private EditionCacheManager eCache;

	@Autowired
	private EditionRolesCacheManager erCache;

	@Autowired
	private PersonCacheManager pCache;

	@Autowired
	private RoomCacheManager rCache;

	@Autowired
	private SessionCacheManager sCache;

	@Autowired
	private ModuleCacheManager mCache;

	@Autowired
	private StudentGroupCacheManager sgCache;

	@Autowired
	@Lazy
	private LabService lService;

	/**
	 * Clears the filter currently stored in the session.
	 *
	 * @param key The key used to store the filter inside the session.
	 */
	public void clearFilter(String key) {
		session.removeAttribute(key);
	}

	/**
	 * Checks the filter DTO for validation errors and stores it in the cache if none are found.
	 *
	 * @param  filter The filter to check and possibly store.
	 * @param  key    The key used to check and store the filter inside the session.
	 * @return        The finalized and validated request table filter.
	 */
	public RequestTableFilterDTO checkAndStoreFilterDTO(RequestTableFilterDTO filter, String key) {
		if (filter == null || filter.hasErrors()) {
			filter = (RequestTableFilterDTO) session.getAttribute(key);
			if (filter == null) {
				filter = new RequestTableFilterDTO();
			}
		} else {
			session.setAttribute(key, filter);
		}

		return filter;
	}

	/**
	 * Updates the partaking information for the current session.
	 *
	 * @param partakes Whether the user that is logged in under the current session partakes.
	 */
	public void updatePartakingStatus(boolean partakes) {
		session.setAttribute("partakes", partakes);
	}

	/**
	 * Gets the current partaking status for the current session.
	 *
	 * @return Whether the user that is logged in partakes in the event.
	 */
	public boolean partakes() {
		Boolean partakes = (Boolean) session.getAttribute("partakes");
		return partakes == null || partakes;
	}

	/**
	 * Gets a mapping from lab ids to the number of requests that are currently in the queue for that lab.
	 *
	 * @param  labs      The labs that we should count over.
	 * @param  assistant The assistant that is requesting this information.
	 * @param  filter    The filter to apply to the request table, given by the user.
	 * @return           The mapping of all lab ids to the number of requests currently still open for that
	 *                   lab.
	 */
	public Map<Long, Long> labRequestCounts(List<Lab> labs, Person assistant,
			RequestTableFilterDTO filter) {
		var language = pr.findProfileForPerson(assistant).getLanguage();
		return labs.stream()
				.collect(Collectors.toMap(
						QueueSession::getId,
						l -> {
							var allowedAssignments = lService.getAllowedAssignmentsInLab(l,
									assistant);
							if (l instanceof AbstractSlottedLab<?>) {
								return rr.countOpenSlotRequests((AbstractSlottedLab<?>) l,
										assistant, filter, allowedAssignments, language);
								// TODO: count exam lab slots
							} else {
								return rr.countNormalRequests(l, assistant, filter, allowedAssignments,
										language);
							}
						}));
	}

	/**
	 * Adds filter specific attributes to the given Model and looks up the set of labs that the currently
	 * authenticated person is involved with based on the active editions they are staff of.
	 *
	 * @param  model The model to fill out for Thymeleaf template resolution.
	 * @param  labs  The labs that are currently active in the request table or {@code null} if the user is
	 *               viewing the request table specifically.
	 * @return       The set of ids of labs that the currently authenticated person is involved with.
	 */
	public List<QueueSession<?>> addFilterAttributes(Model model,
			List<QueueSession<?>> labs) {
		// Fetch the editions currently active and assisted by the user.
		List<EditionDetailsDTO> editions = eCache.getAndIgnoreMissing(eApi.getAllEditionsCurrentlyAssistedBy()
				.map(EditionSummaryDTO::getId).collectList().blockOptional().orElse(List.of()));

		// Get all information necessary to display filters.
		// If no list of labs is provided, take all the labs from the editions.
		labs = (labs == null) ? new ArrayList<>(labs(editions)) : labs;
		List<SessionDetailsDTO> sessions = sessions(labs);
		List<AssignmentDetailsDTO> assignments = assignments(sessions);
		List<RoomDetailsDTO> rooms = rooms(sessions);
		List<PersonSummaryDTO> assistants = assistants(sessions);

		// Add filter lists to the model for the current request.
		model.addAttribute("editions", editions);
		model.addAttribute("labs", View.convert(labs, QueueSessionSummaryDTO.class));
		model.addAttribute("assignments", assignments);
		model.addAttribute("assignmentsWithCourseCodes", assignmentsWithCourseCodes(assignments));
		model.addAttribute("rooms", rooms);
		model.addAttribute("assistants", assistants);

		// Return the list of lab ids for getting requests with.
		return labs;
	}

	/**
	 * Caches information that needs to be fetched about the given list of requests before converting the
	 * entire list of requests into a consumable list of request views. This list of request views can then be
	 * displayed on a page using all the information then available to the Thymeleaf template processor.
	 *
	 * @param  requests The list of requests to process into request views.
	 * @return          The processed list of request views.
	 */
	public List<RequestViewDTO<?>> convertRequestsToView(List<? extends Request<?>> requests) {
		// Pre-load caches with people, sessions, student groups and rooms
		pCache.getAndIgnoreMissing(requests.stream().map(Request::getRequester).distinct());
		sCache.getAndHandleAll(requests.stream().map(r -> r.getSession().getSession()).distinct(),
				lService.deleteSessionsByIds());
		sgCache.getAndIgnoreMissing(requests.stream().map(Request::getStudentGroup).distinct());
		rCache.getAndIgnoreMissing(
				requests.stream().map(Request::getRoom).filter(Objects::nonNull).distinct());

		// Map each of the request to view DTOs that individually do cache requests
		return requests.stream().map(Request::toViewDTO).collect(Collectors.toList());
	}

	/**
	 * Caches information that needs to be fetched about the given page of requests before converting the
	 * entire page of requests into a consumable page of request views. This page of request views can then be
	 * displayed on a page using all the information then available to the Thymeleaf template processor.
	 *
	 * @param  requests The page of requests to process into request views.
	 * @return          The processed page of request views.
	 */
	public Page<RequestViewDTO<?>> convertRequestsToView(Page<? extends Request<?>> requests, long total) {
		return new PageImpl<>(convertRequestsToView(requests.getContent()),
				requests.getPageable(), total);
	}

	/**
	 * Converts a list of requests to a view. Sorts before conversion. Work around for PageImpl not supporting
	 * List to page conversions.
	 *
	 * @param  requestList    The list to convert to a page view
	 * @param  pageable       The pageable object
	 * @param  reversed       Sorts by reversed order of creation time instead
	 * @param  forwardedFirst Puts forwarded requests at the start
	 * @return                The Page of request views.
	 */
	public Page<RequestViewDTO<?>> convertRequestsToView(List<? extends Request<?>> requestList,
			Pageable pageable, boolean reversed, boolean forwardedFirst) {
		Comparator<Request<?>> comparator = Comparator.comparing(Request::getCreatedAt);
		var sortedRequestList = requestList.stream()
				.sorted(reversed ? comparator.reversed() : comparator).toList();
		if (forwardedFirst) {
			sortedRequestList = Stream.concat(
					sortedRequestList.stream()
							.filter(r -> r.getEventInfo().getStatus() == RequestStatus.FORWARDED),
					sortedRequestList.stream()
							.filter(r -> r.getEventInfo().getStatus() != RequestStatus.FORWARDED))
					.toList();
		}
		return PageUtil.toPage(pageable, convertRequestsToView(sortedRequestList));
	}

	/**
	 * Gets the list of all assistants in a list of editions. This list will be necessary for filtering on a
	 * certain assistant within the Queue requests page.
	 *
	 * @param  sessions The list of sessions to find assistants in.
	 * @return          The list of all assistants in the given list of course editions.
	 */
	private List<PersonSummaryDTO> assistants(List<SessionDetailsDTO> sessions) {
		return sessions.stream()
				.flatMap(s -> s.getEditions().stream()).distinct()
				.flatMap(e -> erCache.getRequired(e.getId()).getRoles().stream())
				.filter(role -> Set.of(TA, HEAD_TA, TEACHER, TEACHER_RO).contains(role.getType()))
				.map(RolePersonDetailsDTO::getPerson)
				.distinct()
				.collect(Collectors.toList());
	}

	/**
	 * Gets the list of all rooms used within the given list of sessions. This information can be used to
	 * display requests and filter requests on the room-specific parameter.
	 *
	 * @param  sessions The list of sessions to find all rooms for.
	 * @return          The list of all rooms used within the given sessions.
	 */
	private List<RoomDetailsDTO> rooms(List<SessionDetailsDTO> sessions) {
		return rCache.getAndIgnoreMissing(
				sessions.stream().flatMap(l -> l.getRooms().stream().map(RoomDetailsDTO::getId)).distinct());
	}

	/**
	 * Gets the list of all labs within the given list of editions. This information can be used for
	 * displaying requests and for filtering requests.
	 *
	 * @param  editions The list of editions to find labs by.
	 * @return          The list of all labs within the given editions.
	 */
	private List<Lab> labs(List<EditionDetailsDTO> editions) {
		if (editions.isEmpty()) {
			return List.of();
		}

		var sessions = sApi
				.getActiveSessionsInEditionsWithGracePeriod(
						editions.stream().map(EditionDetailsDTO::getId).collect(Collectors.toList()),
						MAX_GRACE_PERIOD)
				.collectList().block();
		sCache.register(sessions);

		var now = LocalDateTime.now();
		return lr.findAllBySessions(sessions.stream()
				.map(SessionDetailsDTO::getId).distinct()
				.collect(Collectors.toList()))
				.stream().filter(lab -> sCache.get(lab.getSession())
						.map(session -> now
								.isBefore(session.getEndTime().plusMinutes(lab.getEolGracePeriod())))
						.orElse(false))
				.collect(Collectors.toList());
	}

	/**
	 * Gets the session details for the given list of labs.
	 *
	 * @param  labs The labs of which the sessions need to be looked up.
	 * @return      The list of all session details gotten from the list of labs.
	 */
	@Transactional
	protected List<SessionDetailsDTO> sessions(List<? extends QueueSession<?>> labs) {
		return sCache.getAndHandleAll(
				labs.stream().map(QueueSession::getSession).distinct().collect(Collectors.toList()),
				missedIds -> lService.deleteSessionsByIds());
	}

	/**
	 * Gets the list of all assignments acted upon within the given list of editions. This information can be
	 * used for displaying requests and for filtering requests.
	 *
	 * @param  sessions The list of sessions to find assignments for.
	 * @return          The list of all assignments within the given editions.
	 */
	private List<AssignmentDetailsDTO> assignments(List<SessionDetailsDTO> sessions) {
		return aCache.getAndIgnoreMissing(sessions.stream()
				.flatMap(s -> s.getAssignments().stream())
				.map(AssignmentSummaryDTO::getId).distinct());
	}

	/**
	 * Given a list of assignment details, we create a mapping from the assignment to the corresponding course
	 * code.
	 *
	 * @param  assignments The list of assignments to get the edition name for.
	 * @return             The mapping from assignment ids to edition names.
	 */
	private Map<Long, String> assignmentsWithCourseCodes(List<AssignmentDetailsDTO> assignments) {
		if (assignments.isEmpty())
			return Collections.emptyMap();

		return assignments.stream().collect(Collectors.toMap(
				AssignmentDetailsDTO::getId,
				assignment -> eCache
						.getRequired(mCache.getRequired(assignment.getModule().getId()).getEdition().getId())
						.getCourse().getCode()));

	}

}
