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

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

import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessageSendingOperations;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

import nl.tudelft.labracore.api.AssignmentControllerApi;
import nl.tudelft.labracore.api.EditionControllerApi;
import nl.tudelft.labracore.api.SessionControllerApi;
import nl.tudelft.labracore.api.StudentGroupControllerApi;
import nl.tudelft.labracore.api.dto.*;
import nl.tudelft.librador.dto.DTOConverter;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.Request;
import nl.tudelft.queue.model.enums.RequestStatus;
import nl.tudelft.queue.model.events.*;
import nl.tudelft.queue.model.labs.AbstractSlottedLab;
import nl.tudelft.queue.realtime.messages.*;
import nl.tudelft.queue.service.dto.RequestStatusUpdateMessageService;
import reactor.core.publisher.Flux;

@Service
public class WebSocketService {

	@Autowired
	private EditionControllerApi eApi;

	@Autowired
	private SessionControllerApi sApi;

	@Autowired
	private AssignmentControllerApi aApi;

	@Autowired
	private StudentGroupControllerApi sgApi;

	@Autowired
	private SimpMessageSendingOperations messenger;

	@Autowired
	private DTOConverter converter;

	@Autowired
	private RequestStatusUpdateMessageService requestStatusUpdateMessageService;

	/**
	 * Sends a message asynchronously through web-sockets to the user with the given username.
	 *
	 * @param people  The people to send the message to.
	 * @param topic   The topic to send the message over.
	 * @param message The message to send through web-sockets.
	 */
	@Async
	public void sendMessage(Stream<PersonSummaryDTO> people, String topic, Message message) {
		people.forEach(person -> messenger.convertAndSendToUser(person.getUsername(), topic, message));
	}

	/**
	 * Sends a message asynchronously through web-sockets to the user with the given username.
	 *
	 * @param people  The people to send the message to.
	 * @param topic   The topic to send the message over.
	 * @param message The message to send through web-sockets.
	 */
	public void sendMessage(List<PersonSummaryDTO> people, String topic, Message message) {
		people.forEach(person -> messenger.convertAndSendToUser(person.getUsername(), topic, message));
	}

	/**
	 * Sends the given message over web-sockets to all sessions of users that are currently looking at the
	 * request table page. This method uses a few cache lookups to find the editions that a request update is
	 * relevant to and then sends the message to all users with a TA, Head-TA, Teacher or Teacher Read-Only
	 * role in one of said courses.
	 *
	 * @param request The request that is the subject of the message.
	 * @param message The message containing information on the event that occurred for the request.
	 */
	@Async
	public CompletableFuture<Void> sendRequestTableMessage(Request<?> request, Message message) {
		Flux<RolePersonDetailsDTO> rMono;
		if (request instanceof LabRequest labRequest) {
			rMono = aApi.getAssignmentById(labRequest.getAssignment())
					.flatMapMany(dto -> eApi.getEditionParticipants(dto.getModule().getEdition().getId()));
		} else {
			var sMono = sApi.getSessionsById(List.of(request.getSession().getSession())).collectList();
			rMono = sMono
					.flatMapIterable(ss -> ss.stream()
							.flatMap(s -> s.getEditions().stream().map(EditionSummaryDTO::getId))
							.collect(Collectors.toSet()))
					.flatMap(id -> eApi.getEditionParticipants(id));
		}

		sendMessage(Objects.requireNonNull(rMono
				.filter(r -> r != null && Set.of(TEACHER, TEACHER_RO, HEAD_TA, TA).contains(r.getType()))
				.map(RolePersonDetailsDTO::getPerson)
				.distinct().collectList().block()), "/topic/request-table", message);

		return CompletableFuture.completedFuture(null);
	}

	/**
	 * Sends a position update message over web-sockets to all sessions of users that are currently looking at
	 * the lab view page to see their position. A message is sent saying the position is updated.
	 *
	 * @param request The request that the message should be sent about.
	 */
	@Async
	public void sendRequestPositionUpdate(LabRequest request, List<LabRequest> queue) {
		if (!queue.isEmpty()) {
			var index = new AtomicInteger(1);
			var sgs = sgApi.getStudentGroupsById(queue.stream()
					.map(LabRequest::getStudentGroup)
					.collect(Collectors.toList())).collectMap(StudentGroupDetailsDTO::getId).block();

			queue.forEach(r -> {
				sendMessage(sgs.get(r.getStudentGroup()).getMembers().stream()
						.map(RolePersonLayer1DTO::getPerson),
						"/topic/lab/" + request.getSession().getId() + "/position",
						new PositionUpdateMessage(index.getAndIncrement()));
			});
		}

		sendMessage(
				sgApi.getStudentGroupsById(List.of(request.getStudentGroup())).blockFirst()
						.getMembers().stream()
						.map(RolePersonLayer1DTO::getPerson),
				"/topic/lab/" + request.getSession().getId() + "/position",
				new StudentRequestTakenMessage());
	}

	/**
	 * Sends a web socket message for the creation of a new request.
	 *
	 * @param event The event that occurred.
	 */
	public void sendRequestCreated(RequestCreatedEvent event) {
		if (event.getRequest() instanceof LabRequest) {
			// Check if the request is a slotted request. If it is, and the request should not yet be displayed in the
			// request table, don't send a request created event.
			LabRequest request = (LabRequest) event.getRequest();
			if (request.getTimeSlot() != null) {
				AbstractSlottedLab<?> lab = (AbstractSlottedLab<?>) request.getSession();
				if (!request.getTimeSlot().getSlot().getOpensAt().isBefore(
						LocalDateTime.now().plusMinutes(lab.getEarlyOpenTime()))) {
					return;
				}
			}

			sendRequestTableMessage(event.getRequest(), requestStatusUpdateMessageService.convert(event));
		}
	}

	/**
	 * Sends a web socket message for the event of a TA taking a request.
	 *
	 * @param event The event that occurred.
	 */
	public void sendRequestTaken(RequestTakenEvent event) {
		sendRequestTableMessage(event.getRequest(), requestStatusUpdateMessageService.convert(event));
	}

	/**
	 * Sends a web socket message for the event of a TA forwarding a request to anyone else.
	 *
	 * @param event The event that occurred.
	 */
	public void sendRequestForwardedToAny(RequestForwardedToAnyEvent event) {
		sendRequestTableMessage(event.getRequest(), requestStatusUpdateMessageService.convert(event));
	}

	/**
	 * Sends a web socket message for the event of a TA forwarding a request to a specific person.
	 *
	 * @param event The event that occurred.
	 */
	public void sendRequestForwardedToPerson(RequestForwardedToPersonEvent event) {
		sendRequestTableMessage(event.getRequest(), requestStatusUpdateMessageService.convert(event));
	}

	/**
	 * Sends a web socket message for the event of a TA marking a request as "Not Found".
	 *
	 * @param event The event that occurred.
	 */
	public void sendRequestNotFound(StudentNotFoundEvent event) {
		sendRequestTableMessage(event.getRequest(),
				converter.convert(event, RequestFinishedMessage.class).withStatus(RequestStatus.NOTFOUND));
	}

	/**
	 * Sends a web socket message for the event of a TA marking a request as "Not Found".
	 *
	 * @param event The event that occurred.
	 */
	public void sendRequestRevoked(RequestRevokedEvent event) {
		sendRequestTableMessage(event.getRequest(),
				converter.convert(event, RequestRevokedMessage.class));
	}

	/**
	 * Sends a web socket message for the event of a TA marking a request as "Selected".
	 *
	 * @param event The event that occurred.
	 */
	public void sendRequestSelected(RequestSelectedEvent event) {
		sendRequestTableMessage(event.getRequest(),
				converter.convert(event, RequestSelectedMessage.class));
	}

	/**
	 * Sends a web socket message for the event of a TA marking a request as "Not Selected".
	 *
	 * @param event The event that occurred.
	 */
	public void sendRequestNotSelected(RequestNotSelectedEvent event) {
		sendRequestTableMessage(event.getRequest(),
				converter.convert(event, RequestNotSelectedMessage.class));
	}

	/**
	 * Sends a web socket message for the event of marking a request as "Not Picked".
	 *
	 * @param event The event that occurred.
	 */
	public void sendRequestNotPicked(RequestNotPickedEvent event) {
		sendRequestTableMessage(event.getRequest(),
				converter.convert(event, RequestFinishedMessage.class).withStatus(RequestStatus.NOTPICKED));
	}

	/**
	 * Sends a web socket message for the event of a TA marking a request as either "Approved" or "Rejected".
	 *
	 * @param event The event that occurred.
	 */
	public void sendRequestHandled(LabRequest request, RequestHandledEvent event) {
		RequestFinishedMessage message = converter.convert(event, RequestFinishedMessage.class)
				.withStatus(event.status());
		sendRequestTableMessage(event.getRequest(), message);
		sendMessage(
				sgApi.getStudentGroupsById(List.of(request.getStudentGroup())).blockFirst()
						.getMembers().stream()
						.map(RolePersonLayer1DTO::getPerson),
				"/topic/lab/" + request.getSession().getId() + "/position", message);
	}

}
