/*
 * 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 static java.time.LocalDateTime.now;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalDouble;

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

import lombok.*;
import lombok.experimental.SuperBuilder;
import nl.tudelft.librador.dto.view.View;
import nl.tudelft.queue.dto.view.requests.LabRequestViewDTO;
import nl.tudelft.queue.model.enums.Language;
import nl.tudelft.queue.model.enums.OnlineMode;
import nl.tudelft.queue.model.enums.RequestType;
import nl.tudelft.queue.model.labs.ExamLab;
import nl.tudelft.queue.model.labs.Lab;

import org.hibernate.annotations.Cascade;

/**
 * A request that has been made by a StudentGroup, that the database representation of someone asking for
 * help. Part of this class stores it's data directly in the Request table. The lifecycle of a Request, that
 * is being: created, reassigned, forwarded, approved, etc; is done through event sourcing
 * {@link RequestEvent}. Every change in the requests lifecycle append a new event to this log.
 * <p>
 * The entire log is then reran when the Request is recreated by hibernate through an {@link PostLoad} hook,
 * which fills in the remaining transient (not persisted) fields of this class.
 */
@Data
@Entity
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class LabRequest extends Request<Lab> {
	/**
	 * The type of the request (either a question or a submission).
	 */
	@NotNull
	private RequestType requestType;

	/**
	 * The question a student placed to describe the intent of their request.
	 */
	@Lob
	@Column
	@Size(max = 500)
	private String question;

	/**
	 * The Jitsi room this request will take place in.
	 */
	private String jitsiRoom;

	/**
	 * The OnlineMode this request will take place in (Jitsi, MS teams, etc)
	 */
	private OnlineMode onlineMode;

	/**
	 * The language of the request.
	 */
	@NotNull
	@Builder.Default
	@Enumerated(EnumType.STRING)
	private Language language = Language.ANY;

	/**
	 * The timeslot this request occupies. Timeslots can be used as opposed to direct requests to allow for
	 * reserving a TA for a specific time ahead of the lab starting.
	 */
	@ManyToOne
	private TimeSlot timeSlot;

	/**
	 * The Assignment that this request is asking a question about or checking a submission of.
	 */
	@NotNull
	private Long assignment;

	private Long questionId;

	/**
	 * The feedback given through this Request.
	 */
	@Builder.Default
	@ToString.Exclude
	@EqualsAndHashCode.Exclude
	@OneToMany(mappedBy = "request")
	@Cascade(org.hibernate.annotations.CascadeType.DELETE)
	private List<Feedback> feedbacks = new ArrayList<>();

	/**
	 * Gets the waiting time in seconds experienced during this request. The waiting time is defined as the
	 * time between the first time that the request was processed and the creation time of the request.
	 *
	 * @return The waiting time in seconds experienced during this request.
	 */
	public OptionalDouble waitingTime() {
		if (getEventInfo().getFirstProcessedAt() == null || getCreatedAt() == null) {
			return OptionalDouble.empty();
		}

		return OptionalDouble
				.of(ChronoUnit.SECONDS.between(getCreatedAt(), getEventInfo().getFirstProcessedAt()));
	}

	/**
	 * Alias for {@code sensibleWaitingTime(now())}
	 *
	 * @see #sensibleWaitingTime(LocalDateTime)
	 */
	public OptionalDouble sensibleWaitingTime() {
		return sensibleWaitingTime(now());
	}

	/**
	 * Gets the waiting time in seconds that is being experienced, or was experienced for this request. The
	 * waiting time is defined differently depending on the specifics of the request:
	 * <ul>
	 * <li>For once-processed, non-timeslotted requests: the time between the creation of the request and the
	 * first time that the request was processed.</li>
	 * <li>For once-processed, timeslotted requests: the time between the start of the timeslot and the first
	 * time that the request was processed.</li>
	 * <li>For unprocessed, non-timeslotted requests: the time between the creation of the request and
	 * effectiveEndTime.</li>
	 * <li>For unprocessed, timeslotted requests: the time between the start of the timeslot and
	 * effectiveEndTime.</li>
	 * </ul>
	 *
	 * If this request has a timeslot which starts in the future, this method returns an empty optionaldouble.
	 *
	 * @param  effectiveEndTime the effective end time to use
	 *
	 * @return                  The waiting time in seconds this request is experiencing or has experienced.
	 */
	public OptionalDouble sensibleWaitingTime(LocalDateTime effectiveEndTime) {
		LocalDateTime startTime;
		if (getTimeSlot() == null) {
			startTime = getCreatedAt();
		} else {
			startTime = getTimeSlot().getSlot().getOpensAt();
		}

		LocalDateTime endTime;
		if (getEventInfo().getFirstProcessedAt() == null) {
			endTime = effectiveEndTime;
		} else {
			endTime = getEventInfo().getFirstProcessedAt();
		}

		long time = ChronoUnit.SECONDS.between(startTime, endTime);
		if (time < 0) {
			return OptionalDouble.empty();
		}

		return OptionalDouble.of(time);
	}

	@Override
	public boolean isRevokable() {
		boolean isRevokable = true;
		if (getSession() instanceof ExamLab) {
			isRevokable = !getSession().getEnqueueClosed();
		}
		return isRevokable && getEventInfo().getStatus().isPending();
	}

	@Override
	public LabRequestViewDTO toViewDTO() {
		return View.convert(this, LabRequestViewDTO.class);
	}
}
