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

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

import javax.persistence.*;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;

import lombok.*;
import lombok.experimental.SuperBuilder;
import nl.tudelft.labracore.api.dto.SessionDetailsDTO;
import nl.tudelft.librador.SpringContext;
import nl.tudelft.queue.cache.SessionCacheManager;
import nl.tudelft.queue.dto.create.QueueSessionCreateDTO;
import nl.tudelft.queue.dto.patch.QueueSessionPatchDTO;
import nl.tudelft.queue.dto.view.QueueSessionViewDTO;
import nl.tudelft.queue.model.embeddables.LabRequestConstraints;
import nl.tudelft.queue.model.enums.QueueSessionType;

import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.SQLDelete;
import org.hibernate.annotations.Where;
import org.hibernate.validator.constraints.UniqueElements;

@Data
@Entity
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@DiscriminatorColumn(name = "type")
@Where(clause = "deleted_at IS NULL")
@Inheritance(strategy = InheritanceType.JOINED)
@SQLDelete(sql = "UPDATE queue_session SET deleted_at = NOW() WHERE id = ?")
public abstract class QueueSession<R extends Request<?>> {

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;

	/**
	 * The type of the lab.
	 */
	@NotNull
	@Enumerated(EnumType.STRING)
	@Builder.ObtainVia(method = "getLabType")
	@Column(name = "type", insertable = false, updatable = false)
	private final QueueSessionType type = getLabType();

	/**
	 * The session that this lab is related to. The session can be seen as a sort of superclass to this class
	 * as sessions contain all the necessary information about rooms, assignments, etc. needed for a lab to be
	 * displayed in different applications.
	 */
	@NotNull
	private Long session;

	/**
	 * @return the session that this lab is related to
	 */
	public SessionDetailsDTO getSessionDto() {
		return SpringContext.getBean(SessionCacheManager.class).getRequired(session);
	}

	/**
	 * Whether enqueueing to the lab is (manually) closed. This can be toggled by teachers to close exam lab
	 * slot selection or as an all-or-nothing means to end a busy lab early.
	 */
	@NotNull
	@Builder.Default
	private Boolean enqueueClosed = false;

	/**
	 * The time this lab was deleted, or null if this lab was never deleted. This field is used as a means to
	 * soft-delete labs.
	 */
	@Builder.Default
	private LocalDateTime deletedAt = null;

	/**
	 * The various {@link LabRequestConstraint}s applied to this Lab in a single embeddable object.
	 */
	@NotNull
	@Embedded
	@Builder.Default
	private LabRequestConstraints constraints = new LabRequestConstraints();

	/**
	 * The list of modules that this lab accommodates to.
	 */
	@NotEmpty
	@UniqueElements
	@Builder.Default
	@ElementCollection
	private Set<Long> modules = new HashSet<>();

	/**
	 * The mapped list of requests that are created for this lab.
	 */
	@Builder.Default
	@ToString.Exclude
	@EqualsAndHashCode.Exclude
	@Cascade(org.hibernate.annotations.CascadeType.DELETE)
	@OneToMany(mappedBy = "session", cascade = { CascadeType.ALL }, targetEntity = Request.class)
	private List<R> requests = new ArrayList<>();

	@Lob
	private String extraInfo;

	/**
	 * Gets all pending requests in the session.
	 *
	 * @return The list of all requests that currently have a status that reflects it is pending.
	 */
	public List<R> getPendingRequests() {
		return getRequests().stream()
				.filter(r -> r.getEventInfo().getStatus().isPending())
				.collect(Collectors.toList());
	}

	/**
	 * Gets the currently open request for the given person.
	 *
	 * @param  personId The id of the person to lookup an open request for.
	 * @return          An optional either containing the current request for the person or nothing.
	 */
	public Optional<R> getOpenRequestForPerson(Long personId) {
		return getRequests().stream()
				.filter(r -> !r.getEventInfo().getStatus().isFinished() && personId.equals(r.getRequester()))
				.findFirst();
	}

	/**
	 * Gets the currently pending request for the given person.
	 *
	 * @param  personId The id of the person to lookup a pending request for.
	 * @return          An optional either containing the current request for the person or nothing.
	 */
	public Optional<R> getPendingRequestForPerson(Long personId) {
		return getQueue().stream()
				.filter(r -> r.getEventInfo().getStatus().isPending() && personId.equals(r.getRequester()))
				.findFirst();
	}

	/**
	 * Gets all requests for the given person within this lab.
	 *
	 * @param  personId The id of the person to lookup requests for.
	 * @return          A list of all requests for the person within this lab.
	 */
	public List<R> getAllRequestsForPerson(Long personId) {
		return getRequests().stream()
				.filter(r -> personId.equals(r.getRequester()))
				.collect(Collectors.toList());
	}

	/**
	 * Checks whether this lab contains an open request from the person with the given id.
	 *
	 * @param  personId The id of the person to check for.
	 * @return          Whether a request is currently open for the given person.
	 */
	public boolean hasOpenRequestForPerson(Long personId) {
		return getOpenRequestForPerson(personId).isPresent();
	}

	/**
	 * Gets the position a person currently has in the queue, or -1 if the person cannot be found in the
	 * queue.
	 *
	 * @param  personId The id of the person to check the position of.
	 * @return          The position of the person in the current lab queue.
	 */
	public int position(Long personId) {
		List<R> queue = getQueue();
		return queue.stream().filter(r -> personId.equals(r.getRequester())).findFirst()
				.map(queue::indexOf)
				.orElse(-1) + 1;
	}

	/**
	 * Filters out finished requests and assembles a sorted list representing the current queue.
	 *
	 * @return The list of requests representing the current queue.
	 */
	public List<R> getQueue() {
		return getRequests().stream()
				.filter(request -> request.getEventInfo().getStatus().isPending())
				.sorted(Comparator.comparing(Request::getCreatedAt))
				.collect(Collectors.toUnmodifiableList());
	}

	/**
	 * Filters out unhandled open requests and assembles a list of all handled requests.
	 *
	 * @return The list of all requests handled by a TA.
	 */
	public List<R> getHandled() {
		return getRequests().stream()
				.filter(request -> request.getEventInfo().getStatus().isHandled())
				.collect(Collectors.toList());
	}

	/**
	 * Checks whether the given request is allowed according to the restrictions of this lab.
	 *
	 * @param  request The request to check for whether it is allowed to be posted.
	 * @return         Whether the given request should be allowed.
	 */
	public boolean allowsRequest(Request<?> request) {
		return constraints.allowsRequest(request)
				&& Objects.equals(request.getSession().getId(), this.getId());
	}

	/**
	 * Gets the type of this lab.
	 *
	 * @return The type of this lab.
	 */
	protected abstract QueueSessionType getLabType();

	/**
	 * Copies this Lab object into a new {@link QueueSessionCreateDTO} of the right type.
	 *
	 * @param  session The session that this Lab is connected to.
	 * @return         A copy of this Lab object in the form of a create DTO.
	 */
	public abstract QueueSessionCreateDTO<?> copyLabCreateDTO(SessionDetailsDTO session);

	/**
	 * @return A newly constructed instance of a {@link QueueSessionPatchDTO} subclass depending on the type
	 *         of the lab.
	 */
	public abstract QueueSessionPatchDTO<?> newPatchDTO();

	/**
	 * @return A newly constructed {@link QueueSessionViewDTO} constructed based on this session.
	 */
	public abstract QueueSessionViewDTO<?> toViewDTO();
}
