/*
 * 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 java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import lombok.SneakyThrows;
import nl.martijndwars.webpush.Notification;
import nl.martijndwars.webpush.PushService;
import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
import nl.tudelft.labracore.lib.security.user.Person;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.realtime.NotificationPayload;
import nl.tudelft.queue.realtime.Subscription;
import nl.tudelft.queue.realtime.SubscriptionContainer;

@Service
public class QueuePushService {

	@Autowired
	private SubscriptionContainer sc;

	@Autowired
	private PushService ps;

	/**
	 * Creates a notification builder using the given subscription. This notification builder will contain all
	 * the necessary information conveyed by the {@link Subscription} object to send it to the correct push
	 * service.
	 *
	 * @param  subscription The subscription object to copy user push service information from.
	 * @return              The created notification builder.
	 */
	@SneakyThrows
	private Notification.NotificationBuilder createNotification(Subscription subscription) {
		return Notification.builder()
				.endpoint(subscription.getEndpoint())
				.userAuth(subscription.getAuth())
				.userPublicKey(subscription.getP256dhKey());
	}

	/**
	 * Sends all the notifications in the given iterable of notifications. This method uses
	 * {@link PushService#sendAsync(Notification)} in favour of {@link PushService#send(Notification)} so as
	 * to prevent blocking on notifications.
	 *
	 * @param notifications The iterable of all notifications to send.
	 */
	@SneakyThrows
	private void sendNotifications(Iterable<Notification> notifications) {
		for (Notification n : notifications) {
			ps.sendAsync(n);
		}
	}

	/**
	 * Sends a notification to the given person through all their subscriptions.
	 *
	 * @param person The person to send notifications to.
	 * @param f      The function that dictates how to build the notification when a builder is provided.
	 */
	public void sendToPerson(Long person,
			Function<Notification.NotificationBuilder, Notification.NotificationBuilder> f) {
		sendNotifications(sc.get(person).stream()
				.map(this::createNotification).map(f).map(Notification.NotificationBuilder::build)
				.collect(Collectors.toList()));
	}

	/**
	 * Sends a notification to the given list of people through all their subscriptions.
	 *
	 * @param people The people to send notifications to.
	 * @param f      The function that dictates how to build the notification when a builder is provided.
	 */
	public void sendToPeople(Set<Long> people,
			Function<Notification.NotificationBuilder, Notification.NotificationBuilder> f) {
		sendNotifications(people.stream().flatMap(p -> sc.get(p).stream())
				.map(this::createNotification).map(f).map(Notification.NotificationBuilder::build)
				.collect(Collectors.toList()));
	}

	/**
	 * Notifies the users that own the given request of an update in their request.
	 *
	 * @param request The request to which an event happened and of which users need to be notified.
	 * @param payload The payload that the notification should carry.
	 */
	public void notifyAboutRequest(LabRequest request,
			NotificationPayload payload) {
		sendToPerson(request.getRequester(), builder -> builder
				.topic("request-" + request.getId())
				.payload(payload.toJson()));
	}

	/**
	 * Notifies the users that own the given request of an update in their request. This method notifies the
	 * users that the TA was assigned to their request.
	 *
	 * @param request   The request to which an event happened and of which users need to be notified.
	 * @param assistant The assistant that triggered the event.
	 */
	public void notifyAssistantComing(LabRequest request, Person assistant) {
		notifyAboutRequest(request, NotificationPayload.builder()
				.title(assistant.getDisplayName() + " is on their way!")
				.body("The assistant is making their way to your location")
				.tag("request-" + request.getId())
				.build());
	}

	/**
	 * Notifies the users that own the given request of an update in their request. This method notifies the
	 * users that the TA could not answer their request accordingly and thus forwarded the request to a
	 * different TA.
	 *
	 * @param request     The request to which an event happened and of which users need to be notified.
	 * @param assistant   The assistant that triggered the event.
	 * @param forwardedTo The assistant that the request was forwarded to or {@code null} if forwarded to any.
	 */
	public void notifyRequestForwarded(LabRequest request, Person assistant, PersonSummaryDTO forwardedTo) {
		notifyAboutRequest(request, NotificationPayload.builder()
				.title(assistant.getDisplayName() + " forwarded you to " +
						((forwardedTo == null) ? "anyone" : forwardedTo.getDisplayName() + "!"))
				.body("Your request got forwarded by the current TA because " +
						"they couldn't answer your request at this time")
				.tag("request-" + request.getId())
				.build());
	}

	/**
	 * Notifies the users that own the given request of an update in their request. This method notifies the
	 * users that the TA could not find them.
	 *
	 * @param request   The request to which an event happened and of which users need to be notified.
	 * @param assistant The assistant that triggered the event.
	 */
	public void notifyCouldNotFind(LabRequest request, Person assistant) {
		notifyAboutRequest(request, NotificationPayload.builder()
				.title(assistant.getDisplayName() + " could not find you!")
				.body("The assistant could not find you, did you enter the right room?")
				.tag("request-" + request.getId())
				.build());
	}

}
