/*
 * 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.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import nl.tudelft.labracore.api.dto.RoomDetailsDTO;
import nl.tudelft.labracore.api.dto.SessionDetailsDTO;
import nl.tudelft.queue.cache.EditionCacheManager;
import nl.tudelft.queue.cache.RoomCacheManager;
import nl.tudelft.queue.dto.view.statistics.BucketStatisticsViewDto;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.RequestEvent;
import nl.tudelft.queue.model.events.EventWithAssistant;
import nl.tudelft.queue.model.labs.Lab;

@Service
@RequiredArgsConstructor
public class SessionStatusService {

	private final RequestService requestService;

	private final SessionService sessionService;

	private final EditionCacheManager eCache;

	private final RoomCacheManager rCache;

	private final EditionStatusService ess;

	/**
	 * Gets the time in milliseconds since the last interaction for all assistants who have <b>ONLY
	 * ALREADY</b> interacted with something in the lab.
	 *
	 * @param  qs       The session to consider
	 * @param  editions The editions to consider
	 * @return          A mapping of (already participating) assistant IDs to the time in milliseconds since
	 *                  their last request interaction.
	 */
	public Map<Long, Long> getTimeSinceLastInteraction(Lab qs, Set<Long> editions) {

		return requestService.getLabRequestsForEditions(qs.getRequests(), editions)
				.stream()
				.flatMap(request -> request.getEventInfo().getEvents().stream())
				.filter(event -> event instanceof EventWithAssistant)
				.map(event -> (EventWithAssistant) event)
				.collect(Collectors.toMap(
						EventWithAssistant::getAssistant,
						event -> ((RequestEvent<?>) event).getTimestamp().until(LocalDateTime.now(),
								ChronoUnit.MILLIS),
						Long::min));

	}

	/**
	 * Method responsible for creating a list of dtos where each one represents a bucket containing
	 * information about how many requests occured within that timespan. It also segregates between the
	 * courses they took place in.
	 *
	 * The bucket function that is used here ONLY considers requests if they were created within the bucket.
	 * This is not a running statistic.
	 *
	 * @param  qSession            The lab to consider
	 * @param  editions            The editions that should be filtered for
	 * @param  bucketSizeInMinutes The number of minutes within a bucket.
	 * @return                     A list of dtos, each one representing a bucket
	 */
	public List<BucketStatisticsViewDto> createRequestDistribution(Lab qSession,
			Set<Long> editions,
			long bucketSizeInMinutes) {

		final BiFunction<List<LabRequest>, LocalDateTime, Map<String, Long>> bucketFunction = (labRequests,
				bucketEnd) -> {

			// bucket function does not consider `bucketEnd` since this is not a running statistic.
			return labRequests
					.stream()
					.collect(Collectors.toMap(
							rq -> eCache
									.getRequired(requestService.getEditionForLabRequest(rq).getId())
									.getCourse().getName(),
							rq -> 1L,
							Long::sum));
		};

		return createBucketsForStatistic(qSession, editions, bucketSizeInMinutes, bucketFunction, false);
	}

	/**
	 * Method responsible for creating a list of dtos where each one represents a bucket containing
	 * information about how many requests occured within that timespan. It also segregates between the
	 * courses they took place in.
	 *
	 * The bucket function considers all requests that are currently considered as active (not handled). It
	 * also considers handled requests if the handled at time is later than the bucket end.
	 *
	 * @param  qSession            The lab to consider
	 * @param  editions            The editions that should be filtered for
	 * @param  bucketSizeInMinutes The number of minutes within a bucket.
	 * @return                     A list of dtos, each one representing a bucket
	 */
	public List<BucketStatisticsViewDto> createBuildingDistribution(Lab qSession,
			Set<Long> editions,
			long bucketSizeInMinutes) {

		final BiFunction<List<LabRequest>, LocalDateTime, Map<String, Long>> bucketFunction = (labRequests,
				bucketEnd) -> {
			List<RoomDetailsDTO> rooms = labRequests.stream()
					.filter(rq -> rq.getRoom() != null)
					.filter(rq -> rq.getEventInfo().getHandledAt() == null
							|| rq.getEventInfo().getHandledAt().isAfter(bucketEnd))
					.map(rq -> rCache.getRequired(rq.getRoom()))
					.toList();

			return rooms.stream()
					.collect(Collectors.toMap(
							room -> room.getBuilding().getName(),
							room -> 1L,
							Long::sum));
		};

		return createBucketsForStatistic(qSession, editions, bucketSizeInMinutes, bucketFunction, true);
	}

	/**
	 * This bucket function assumes a few things. At a given timestamp t, we consider all previous requests
	 * within the last 1 hour. Requests that fit this criteria have their statistics averaged for that bucket.
	 *
	 * @param  qSession            The qSession to consider
	 * @param  editions            The editions to consider
	 * @param  bucketSizeInMinutes The bucket size in minutes
	 * @return                     Bucketed statistics corresponding to the above information
	 */
	public List<BucketStatisticsViewDto> createWaitProcessingTimeDistribution(Lab qSession,
			Set<Long> editions, long bucketSizeInMinutes) {
		final LocalDateTime now = LocalDateTime.now();

		// running statistic which considers the last 1 hour to avoid outlier skewing.
		BiFunction<List<LabRequest>, LocalDateTime, Map<String, Long>> bucketFunction = (labRequests,
				bucketEnd) -> {
			List<LabRequest> requestsInTheLastThirtyMinutes = labRequests.stream()
					.filter(rq -> rq.getCreatedAt().isAfter(now.minusHours(1L))).toList();
			return Map.of(
					"Average Wait Time",
					ess.averageWaitingTime(requestsInTheLastThirtyMinutes, ChronoUnit.MINUTES, bucketEnd),
					"Average Processing Time",
					ess.averageProcessingTime(requestsInTheLastThirtyMinutes, ChronoUnit.MINUTES, bucketEnd));
		};

		return createBucketsForStatistic(qSession, editions, bucketSizeInMinutes, bucketFunction, true);
	}

	/**
	 * Creates non-overlapping buckets for bucketed statistics.
	 *
	 * @param  qSession            The session to be consdidered
	 * @param  editions            The editions to be considered
	 * @param  bucketSizeInMinutes The bucket size in minutes for each bucket
	 * @param  bucketFunction      The function to transform a list of requests within a bucket to statistic
	 *                             data.
	 * @return                     A list of bucketed statistics which have information for that specific
	 *                             timespan
	 */
	private List<BucketStatisticsViewDto> createBucketsForStatistic(Lab qSession,
			Set<Long> editions,
			long bucketSizeInMinutes,
			BiFunction<List<LabRequest>, LocalDateTime, Map<String, Long>> bucketFunction,
			boolean runningStatistic) {
		LocalDateTime now = LocalDateTime.now();
		var requests = requestService.getLabRequestsForEditions(qSession.getRequests(), editions);

		if (requests.isEmpty() || bucketSizeInMinutes <= 0L) {
			return new ArrayList<>();
		}

		DateTimeFormatter dateFormat = DateTimeFormatter.ofPattern("HH:mm");
		SessionDetailsDTO sessionDTO = sessionService.getSessionDTOFromSession(qSession);
		LocalDateTime minTime = sessionDTO.getStart();
		LocalDateTime maxTime = sessionDTO.getEndTime().isAfter(now) ? now
				: sessionDTO.getEndTime();

		if (minTime.isAfter(maxTime)) {
			return new ArrayList<>(); // this occurs when lab statistics are viewed before the session starts.
		}

		return Stream.iterate(minTime,
				time -> time.isEqual(minTime) || !time.plusMinutes(bucketSizeInMinutes).isAfter(maxTime),
				time -> time.plusMinutes(bucketSizeInMinutes)).map(bucketStart -> {
					LocalDateTime tempEnd = bucketStart.plusMinutes(bucketSizeInMinutes);
					LocalDateTime bucketEnd = tempEnd.isAfter(maxTime) ? maxTime : tempEnd;

					List<LabRequest> requestsInBucket;
					if (runningStatistic) {
						requestsInBucket = requests.stream()
								.filter(r -> labRequestBeforeOrInBucket(r, bucketEnd))
								.toList();
					} else {
						requestsInBucket = requests.stream()
								.filter(r -> labRequestCreatedInBucket(r, bucketStart, bucketEnd))
								.toList();
					}

					Map<String, Long> bucketData = bucketFunction.apply(requestsInBucket, bucketEnd);

					String bucketLabel = bucketStart.format(dateFormat) +
							" - " +
							bucketEnd.format(dateFormat);
					return new BucketStatisticsViewDto(bucketData, bucketLabel);
				}).toList();
	}

	private boolean labRequestCreatedInBucket(LabRequest r, LocalDateTime bucketStart,
			LocalDateTime bucketEnd) {
		return r.getCreatedAt().isEqual(bucketStart)
				|| r.getCreatedAt().isAfter(bucketStart) && r.getCreatedAt().isBefore(bucketEnd);
	}

	private boolean labRequestBeforeOrInBucket(LabRequest r, LocalDateTime bucketEnd) {
		return r.getCreatedAt().isBefore(bucketEnd);
	}

}
