/*
 * 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 java.time.LocalDateTime.now;
import static nl.tudelft.labracore.api.dto.RolePersonDetailsDTO.TypeEnum.*;
import static nl.tudelft.queue.misc.QueueSessionStatus.*;

import java.io.IOException;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import javax.transaction.Transactional;

import nl.tudelft.labracore.api.EditionControllerApi;
import nl.tudelft.labracore.api.ModuleControllerApi;
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.Person;
import nl.tudelft.librador.dto.view.View;
import nl.tudelft.queue.cache.EditionCacheManager;
import nl.tudelft.queue.cache.EditionCollectionCacheManager;
import nl.tudelft.queue.cache.SessionCacheManager;
import nl.tudelft.queue.csv.*;
import nl.tudelft.queue.dto.create.QueueSessionCreateDTO;
import nl.tudelft.queue.dto.create.labs.AbstractSlottedLabCreateDTO;
import nl.tudelft.queue.dto.patch.QueueSessionPatchDTO;
import nl.tudelft.queue.dto.patch.labs.AbstractSlottedLabPatchDTO;
import nl.tudelft.queue.dto.view.CalendarEntryViewDTO;
import nl.tudelft.queue.dto.view.QueueEditionDetailsDTO;
import nl.tudelft.queue.dto.view.requests.LabRequestViewDTO;
import nl.tudelft.queue.dto.view.requests.SelectionRequestViewDTO;
import nl.tudelft.queue.misc.QueueSessionStatus;
import nl.tudelft.queue.model.QueueSession;
import nl.tudelft.queue.model.TimeSlot;
import nl.tudelft.queue.model.embeddables.AllowedRequest;
import nl.tudelft.queue.model.enums.OnlineMode;
import nl.tudelft.queue.model.enums.RequestType;
import nl.tudelft.queue.model.enums.SelectionProcedure;
import nl.tudelft.queue.model.labs.AbstractSlottedLab;
import nl.tudelft.queue.model.labs.CapacitySession;
import nl.tudelft.queue.model.labs.ExamLab;
import nl.tudelft.queue.model.labs.Lab;
import nl.tudelft.queue.model.labs.RegularLab;
import nl.tudelft.queue.model.misc.Presentation;
import nl.tudelft.queue.properties.QueueProperties;
import nl.tudelft.queue.repository.LabRepository;
import nl.tudelft.queue.repository.LabRequestConstraintRepository;
import nl.tudelft.queue.repository.PresentationRepository;
import nl.tudelft.queue.repository.QueueSessionRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import org.springframework.ui.Model;
import org.springframework.web.multipart.MultipartFile;

@Service
public class LabService {

	@Autowired
	private QueueProperties qp;

	@Autowired
	private LabRequestConstraintRepository lrcr;

	@Autowired
	private SessionControllerApi sApi;

	@Autowired
	private EditionCacheManager eCache;

	@Autowired
	private EditionCollectionCacheManager ecCache;

	@Autowired
	private SessionCacheManager sCache;

	@Autowired
	private EditionService es;

	@Autowired
	private EditionControllerApi eApi;

	@Autowired
	private FileService fs;

	@Autowired
	private RequestTableService rts;

	@Autowired
	private TimeSlotService tss;

	@Autowired
	private LabRepository lr;

	@Autowired
	private QueueSessionRepository qsr;

	@Autowired
	private PresentationRepository pr;

	@Autowired
	private CapacitySessionService css;

	@Autowired
	private RoleDTOService rService;

	@Autowired
	private ModuleControllerApi mApi;

	@Autowired
	private PersonControllerApi pca;

	public enum SessionType {
		REGULAR,
		SHARED
	}

	/**
	 * Sets the organizational unit that the given lab relates to in the given model. This could either be an
	 * Edition or an EditionCollection.
	 *
	 * @param lab   The lab from which to extract the organizational unit.
	 * @param model The model to set for Thymeleaf resolution.
	 */
	public void setOrganizationInModel(QueueSession<?> lab, Model model) {
		var session = sCache.getRequired(lab.getSession(), id -> deleteSession(lab));
		setOrganizationInModel(session, model);
	}

	/**
	 * Sets the organizational unit that the given session relates to in the given model. This could either be
	 * an Edition or an EditionCollection.
	 *
	 * @param session The session from which to extract the organizational unit.
	 * @param model   The model to set for Thymeleaf resolution.
	 */
	public void setOrganizationInModel(SessionDetailsDTO session, Model model) {
		model.addAttribute("edition",
				(session.getEdition() != null)
						? es.queueEditionDTO(eCache.getRequired(session.getEdition().getId()),
								QueueEditionDetailsDTO.class)
						: null);
		model.addAttribute("ec",
				(session.getEditionCollection() != null)
						? ecCache.getRequired(session.getEditionCollection().getId())
						: null);
	}

	/**
	 * Converts the given session to a CSV with a list of the requests in that lab.
	 *
	 * @param  qSession    The session to convert to a CSV.
	 * @return             The resource representing the CSV file.
	 * @throws IOException when something goes wrong while reading or writing the file.
	 */
	public Resource sessionToCsv(QueueSession<?> qSession) throws IOException {
		String[] header = new String[0];
		if (qSession instanceof CapacitySession) {
			header = SelectionRequestViewDTO.csvHeader();
		} else if (qSession instanceof Lab) {
			header = LabRequestViewDTO.csvHeader();
		}
		String[] finalHeader = header;
		String name = sCache.getRequired(qSession.getSession(), id -> deleteSession(qSession)).getName();
		var requests = rts.convertRequestsToView(qSession.getRequests());
		return fs.writeTempResource("session-" + name + "-" + requests.hashCode() + ".csv",
				() -> CsvHelper.writeCsvValues(finalHeader, requests, ','));
	}

	public List<Resource> sessionDTOToCsv(SessionSummaryDTO session) throws IOException {
		var sessions = qsr.findAllBySessions(List.of(session.getId()));
		List<Resource> list = new ArrayList<>();
		for (QueueSession<?> queueSession : sessions) {
			Resource resource = sessionToCsv(queueSession);
			list.add(resource);
		}
		return list;
	}

	/**
	 * Checks whether the given session contains the room with the given id.
	 *
	 * @param  session The session that is to be checked for rooms.
	 * @param  roomId  The id of the room to check for.
	 * @return         Whether the given room is in the given session.
	 */
	public boolean containsRoom(SessionDetailsDTO session, Long roomId) {
		return session.getRooms().stream()
				.anyMatch(r -> Objects.equals(r.getId(), roomId));
	}

	/**
	 * Checks whether the given lab allows the assignment with the given id.
	 *
	 * @param  lab        The lab that needs to be checked.
	 * @param  assignment The assignment that needs to be checked for.
	 * @return            Whether the given assignment is active in the given lab.
	 */
	public boolean containsAssignment(Lab lab, Long assignment) {
		return lab.getAllowedRequests().stream()
				.anyMatch(ar -> Objects.equals(ar.getAssignment(), assignment));
	}

	/**
	 * Checks whether the given lab allows a request with the given assignment and request type.
	 *
	 * @param  lab        The lab that needs to be checked.
	 * @param  assignment The assignment that needs to be checked for.
	 * @param  type       The type that needs to be checked for.
	 * @return            Whether the given assignment and request type combination are allowed in the lab.
	 */
	public boolean containsAllowedRequest(Lab lab, Long assignment, RequestType type) {
		return lab.getAllowedRequests().contains(new AllowedRequest(assignment, type));
	}

	/**
	 * Checks whether the given lab is open to enqueueing of students. This could check whether the lab slot
	 * is open or whether slot selection for the lab is open.
	 *
	 * @param  qSession The lab to check.
	 * @return          Whether the lab is open to enqueueing.
	 */
	public QueueSessionStatus getLabStatus(QueueSession<?> qSession) {
		if (qSession.getEnqueueClosed()) {
			return GENERIC_CLOSED;
		}

		SessionDetailsDTO session = sCache.getRequired(qSession.getSession(), id -> deleteSession(qSession));

		if (session.getEndTime().isBefore(now())) {
			return FINISHED_SESSION;
		} else if (qSession instanceof RegularLab &&
				now().isBefore(session.getStart())) {
			return INACTIVE_SESSION;
		} else if (qSession instanceof AbstractSlottedLab<?> &&
				now().isBefore(
						((AbstractSlottedLab<?>) qSession).getSlottedLabConfig().getSelectionOpensAt())) {
			return SLOT_SELECTION_CLOSED;
		} else if (qSession instanceof CapacitySession &&
				(((CapacitySession) qSession).getCapacitySessionConfig().getEnrolmentClosesAt()
						.isBefore(now())
						|| now().isBefore(((CapacitySession) qSession).getCapacitySessionConfig()
								.getEnrolmentOpensAt()))) {
			return ENROLMENT_CLOSED;
		}

		return OPEN;
	}

	@Transactional
	public <D extends QueueSession<?>> List<D> createSessions(QueueSessionCreateDTO<D> dto, Long ouId,
			SessionType sessionType) {
		ArrayList<D> sessions = new ArrayList<>();
		sessions.add(createOneSession(dto, ouId, sessionType));

		if (dto.getRepeatForXWeeks() != null && dto.getRepeatForXWeeks() > 0) {
			for (int i = 0; i < dto.getRepeatForXWeeks(); i++) {
				dto.plusWeeks(1L);
				sessions.add(createOneSession(dto, ouId, sessionType));
			}
		}

		return sessions;
	}

	/**
	 * Creates a new lab using the create-DTO filled in when a user submitted the 'Create a new lab' form.
	 * Additional information to this method are the type of organizational unit that the lab related to and
	 * the ID of said organizational unit.
	 *
	 * @param  dto         The create DTO that was submitted by a user through the create page.
	 * @param  ouId        The ID of the organizational unit to create a lab for.
	 * @param  sessionType The type of session that is getting created (either a regular/single session or a
	 *                     shared one).
	 * @return             The created lab.
	 */
	@Transactional
	public <D extends QueueSession<?>> D createOneSession(QueueSessionCreateDTO<D> dto, Long ouId,
			SessionType sessionType) {
		D lab = dto.apply();

		// Get the assignments and rooms as ID DTOs.
		var assignments = dto.assignmentIdDTOs();
		var rooms = dto.roomIdDTOs();

		// Add a session backing up this lab in Labracore.
		switch (sessionType) {
			case REGULAR -> lab.setSession(addSingleSession(dto, ouId, assignments, rooms));
			case SHARED -> lab.setSession(addSharedSession(dto, ouId, assignments, rooms));
		}

		// Save the lab.
		lab = qsr.save(lab);

		// Create the constraints for the created lab.
		lab.getConstraints().setLab(lab);
		lrcr.saveAll(lab.getConstraints().toList());

		// Create the time slots for a slotted lab.
		if (dto instanceof AbstractSlottedLabCreateDTO && lab instanceof AbstractSlottedLab) {
			tss.createSlotsForLab((AbstractSlottedLabCreateDTO<?>) dto, (AbstractSlottedLab<?>) lab);
		}

		return lab;
	}

	/**
	 * Updates the information of a lab by updating the Queue specific Lab configurations and updating the
	 * session bound to the lab in Labracore.
	 *
	 * @param dto     The dto containing information on how to change the lab.
	 * @param session The lab that is to be edited.
	 */
	@Transactional
	public <D extends QueueSession<?>> void updateSession(QueueSessionPatchDTO<D> dto, D session) {
		// Get the assignments and rooms as ID DTOs.
		Set<AssignmentIdDTO> assignments = dto.assignmentIdDTOs() == null ? null
				: new HashSet<>(dto.assignmentIdDTOs());
		Set<RoomIdDTO> rooms = dto.roomIdDTOs() == null ? null : new HashSet<>(dto.roomIdDTOs());

		// Patch the session on the Labracore side of things.
		sApi.patchSession(new SessionPatchDTO()
				.name(dto.getName())
				.start((dto.getSlot() != null) ? dto.getSlot().getOpensAt() : null)
				.end((dto.getSlot() != null) ? dto.getSlot().getClosesAt() : null)
				.assignments(assignments)
				.rooms(rooms), session.getSession()).block();
		session.setExtraInfo(dto.getExtraInfo());

		if (dto instanceof AbstractSlottedLabPatchDTO<?> slottedLabPatchDTO
				&& session instanceof AbstractSlottedLab<?> slottedLab
				&& dto.getSlot() != null) {
			tss.updateSlots(slottedLabPatchDTO, slottedLab);
		}

		dto.apply(session);
	}

	public void updateCapacitySessionRequests(CapacitySession session,
			SelectionProcedure oldProcedure) {
		if (!session.getCapacitySessionConfig().getProcedure().equals(
				oldProcedure)) {
			css.resetRequests(session);
		}
	}

	/**
	 * Delete a lab by setting the deletedAt to the current date.
	 *
	 * @param session The lab to delete.
	 */
	@Transactional
	public void deleteSession(QueueSession<?> session) {
		qsr.delete(session);
	}

	/**
	 * Callback for deleting a list of labs/sessions by setting the deletedAt to the current date.
	 * <p>
	 * <b>NOTE:</b> This method is not transactional, as it merely returns a lambda, so it should be used in a
	 * transactional context.
	 * </p>
	 *
	 * @return A consumer which deletes sessions based on the list of ids provided
	 */
	public Consumer<List<Long>> deleteSessionsByIds() {
		return ids -> qsr.deleteAllById(ids);
	}

	/**
	 * Adds a regular session in Labracore to represent the Lab. This session is created every time a new lab
	 * gets created, just to back a Queue lab with a core session.
	 *
	 * @param  dto         The DTO used for creating the lab, containing information about the intended
	 *                     session.
	 * @param  editionId   The id of the edition to create this regular lab in.
	 * @param  assignments The assignments that are to be registered for the session.
	 * @param  rooms       The rooms that are to be registered for the session.
	 * @return             The id of the created session.
	 */
	private Long addSingleSession(QueueSessionCreateDTO<?> dto, Long editionId,
			List<AssignmentIdDTO> assignments, List<RoomIdDTO> rooms) {
		return sApi.addSingleSession(new SingleSessionCreateDTO()
				.name(dto.getName())
				.description("Queue lab session")
				.start(dto.getSlot().getOpensAt())
				.endTime(dto.getSlot().getClosesAt())
				.assignments(assignments)
				.rooms(rooms)
				.edition(new EditionIdDTO().id(editionId)))
				.block();
	}

	/**
	 * Adds a shared session in Labracore to represent the Lab. This session is created every time a new lab
	 * gets created, just to back a Queue lab with a core session.
	 *
	 * @param  dto                 The DTO used for creating the lab, containing information about the
	 *                             intended session.
	 * @param  editionCollectionId The id of the edition collection to create this shared lab in.
	 * @param  assignments         The assignments that are to be registered for the session.
	 * @param  rooms               The rooms that are to be registered for the session.
	 * @return                     The id of the created session.
	 */
	private Long addSharedSession(QueueSessionCreateDTO<?> dto, Long editionCollectionId,
			List<AssignmentIdDTO> assignments, List<RoomIdDTO> rooms) {
		return sApi.addSharedSession(new SharedSessionCreateDTO()
				.name(dto.getName())
				.description("Queue lab session")
				.start(dto.getSlot().getOpensAt())
				.endTime(dto.getSlot().getClosesAt())
				.assignments(assignments)
				.rooms(rooms)
				.editionCollection(new EditionCollectionIdDTO().id(editionCollectionId)))
				.block();
	}

	/**
	 * Converts labs to {@link CalendarEntryViewDTO} to be rendered in a calendar.
	 *
	 * @param  labs The labs to be converted.
	 * @return      A list of labs
	 */
	public List<CalendarEntryViewDTO> convertToCalendarEntries(List<Lab> labs) {
		return labs.stream().map(lab -> View.convert(lab, CalendarEntryViewDTO.class))
				.collect(Collectors.toList());
	}

	/**
	 * Get all the labs which are taking place within a certain period.
	 *
	 * @param  start Start of the period for which to retrieve all the labs.
	 * @param  end   End of the period for which to retrieve all the labs.
	 * @return       A list of labs which are all occurring within the specified period.
	 */
	public List<Lab> getAllLabsWithinPeriod(LocalDateTime start, LocalDateTime end) {
		var editions = eCache
				.getAndIgnoreMissing(Objects.requireNonNull(eApi.getAllEditionsActiveDuringPeriod(start, end)
						.map(EditionSummaryDTO::getId).collectList().block()));
		var sessions = editions.stream().map(EditionDetailsDTO::getSessions).flatMap(List::stream)
				.toList();

		return lr.findAllBySessions(
				sessions.stream().map(SessionSummaryDTO::getId).collect(Collectors.toList()));
	}

	/**
	 * Count all the ongoing labs from a list of labs.
	 *
	 * @param  labs The labs to count for.
	 * @return      The number of labs which are currenlty open.
	 */
	@Transactional
	public long countOngoingLabs(List<Lab> labs) {
		LocalDateTime now = LocalDateTime.now();
		List<SessionDetailsDTO> sessions = sCache.getAndHandleAll(labs.stream().map(QueueSession::getSession),
				deleteSessionsByIds());
		return sessions.stream().filter(s -> s.getStart().isBefore(now) &&
				s.getEndTime().isAfter(now)).count();
	}

	/**
	 * Count the number of labs in a list for which slot selection is currently allowed.
	 *
	 * @param  labs The labs to count for.
	 * @return      The number of labs for which slots currently can be reserved.
	 */
	public long countLabsWithOpenSlotSelection(List<Lab> labs) {
		LocalDateTime now = LocalDateTime.now();
		return labs.stream().filter(l -> {
			switch (l.getType()) {
				case SLOTTED, EXAM -> {
					AbstractSlottedLab<TimeSlot> lab = (AbstractSlottedLab<TimeSlot>) l;
					return lab.getSlottedLabConfig().getSelectionOpensAt().isBefore(now)
							&& sCache.getRequired(lab.getSession(), id -> deleteSession(lab)).getEndTime()
									.isAfter(now);
				}
				default -> {
					return false;
				}
			}
		}).count();
	}

	/**
	 * Retrieves all the assignments an assistant is allowed to take. Mainly useful for shared labs where the
	 * TA is not a TA for all the courses.
	 *
	 * @param  lab       The lab for which to find the assignments.
	 * @param  assistant The TA for whom to find the assignments.
	 * @return           A list of assignments a TA is allowed to get.
	 */
	public List<AssignmentSummaryDTO> getAllowedAssignmentsInLab(Lab lab, Person assistant) {
		var sessionDetails = sCache.getRequired(lab.getSession(), id -> deleteSession(lab));
		var allowedAssignments = sessionDetails.getAssignments().stream().toList();
		if (sessionDetails.getEditionCollection() != null) {
			var collection = ecCache.getRequired(sessionDetails.getEditionCollection().getId());
			var roles = collection.getEditions().stream().map(e -> rService.rolesForPersonInEdition(e,
					assistant)).flatMap(List::stream).filter(rService::isStaff).toList();
			allowedAssignments = roles.stream()
					.map(r -> mApi.getModuleByEdition(r.getEdition().getId()).collectList().block())
					.filter(Objects::nonNull)
					.flatMap(Collection::stream).map(ModuleDetailsDTO::getAssignments)
					.flatMap(List::stream).toList();
		}
		return allowedAssignments;
	}

	/**
	 * Gets the online modes in a lab session.
	 *
	 * @param  labSessionId The ID of the lab session
	 * @return              The online modes associated to the lab.
	 */
	public Set<OnlineMode> getOnlineModesInLabSession(long labSessionId) {
		var labs = lr.findAllBySessions(List.of(labSessionId));
		return labs.stream().map(Lab::getOnlineModes).flatMap(Set::stream).collect(Collectors.toSet());
	}

	/**
	 * Adds students, identified by their netid in a CSV file to an exam lab to give them priority during the
	 * review.
	 *
	 * @param  exam                The exam to which they should be added.
	 * @param  file                The csv file containing netid's
	 * @throws EmptyCsvException
	 * @throws InvalidCsvException
	 */
	@Transactional
	public void addStudentsToReview(ExamLab exam, MultipartFile file)
			throws EmptyCsvException, InvalidCsvException {
		List<PersonDetailsDTO> persons = CsvPerson.readCsv(file).stream()
				.map(s -> pca.getPersonByUsername(s.getUsername()).block()).toList();
		exam.setPickedStudents(persons.stream().map(PersonDetailsDTO::getId).collect(Collectors.toSet()));
	}

	@Transactional
	public Presentation getLabPresentation(Lab lab) {
		if (lab.getPresentation() == null) {
			Presentation presentation = new Presentation();
			presentation.setLab(lab);
			lab.setPresentation(presentation);
			pr.save(presentation);
		}
		return lab.getPresentation();
	}

}
