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

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

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

import com.beust.jcommander.Strings;

import jakarta.transaction.Transactional;
import jakarta.validation.ValidationException;
import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
import nl.tudelft.labracore.api.dto.RolePersonLayer1DTO;
import nl.tudelft.labracore.api.dto.SessionDetailsDTO;
import nl.tudelft.labracore.api.dto.StudentGroupDetailsDTO;
import nl.tudelft.librador.dto.DTOConverter;
import nl.tudelft.queue.cache.PersonCacheManager;
import nl.tudelft.queue.cache.SessionCacheManager;
import nl.tudelft.queue.cache.StudentGroupCacheManager;
import nl.tudelft.queue.dto.patch.FeedbackPatchDTO;
import nl.tudelft.queue.dto.view.FeedbackViewDTO;
import nl.tudelft.queue.model.Feedback;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.repository.FeedbackRepository;
import nl.tudelft.queue.repository.LabRequestRepository;

@Service
public class FeedbackService {

	@Autowired
	private PersonCacheManager pCache;

	@Autowired
	private StudentGroupCacheManager groupCache;

	@Autowired
	private FeedbackRepository fr;

	@Autowired
	private LabRequestRepository labRequestRepository;

	@Autowired
	private PermissionService ps;

	@Autowired
	@Lazy
	private LabService ls;

	@Autowired
	private SessionService sessionService;

	@Autowired
	private SessionCacheManager sessionCache;

	@Autowired
	private DTOConverter converter;

	/**
	 * Finds all assistants that are involved in the given request. This list of people is generated for
	 * students to be able to give feedback to TAs that helped them. This list is ordered by event occurrence:
	 * the last occurred event will see its assistant at the top of the list. It also excludes assistants
	 * which could not find the student. This is done to ensure that students do not give feedback to a TA who
	 * never interacted with them.
	 *
	 *
	 * @param  requestId The id of the request to find all assistants for.
	 * @return           A list of person summaries representing the various assistants helping out with the
	 *                   given request.
	 */
	public List<PersonSummaryDTO> assistantsInvolvedInRequest(Long requestId) {
		return pCache.getAndIgnoreMissing(labRequestRepository.findById(requestId).stream()
				.flatMap(request -> request.getEventInfo().involvedAssistants().stream()));
	}

	/**
	 * Counts the occurrences of each rating (in 5 buckets) and returns the counts of ratings for the
	 * assistant with the given id for a specific list of requests
	 *
	 * @param  requests    a list of requests for which to count ratings
	 * @param  assistantId The id of the assistant for which to count ratings.
	 * @return             A list with five elements,each one representing a star and each element is the
	 *                     number of occurances of that star.
	 */
	private List<Integer> countRatings(Long assistantId, List<LabRequest> requests) {
		List<Feedback> feedbacks = requests.stream().flatMap(rq -> rq.getFeedbacks().stream())
				.filter(fb -> Objects.equals(fb.getId().getAssistantId(), assistantId)).toList();
		return countRatings(feedbacks);
	}

	/**
	 * Counts the occurences of each of the five-star ratings and returns the counts of ratings for the given
	 * lists of feedbacks.
	 *
	 * @param  feedbacks The list of feedbacks to consider.
	 * @return           The number of ratings (pertaining to a scale from 1 to 5) in buckets.
	 */
	public List<Integer> countRatings(List<Feedback> feedbacks) {
		List<Integer> counts = new ArrayList<>(List.of(0, 0, 0, 0, 0));

		for (Feedback feedback : feedbacks.stream().filter(fb -> fb.getRating() != null).toList()) {
			counts.set(feedback.getRating() - 1, counts.get(feedback.getRating() - 1) + 1);
		}

		return counts;
	}

	/**
	 * Get the avg star rating of an assistant w.r.t a list of requests.
	 *
	 * @param  personId The person you want to get the avg star rating for.
	 * @param  requests The requests you want to consider
	 * @return          The average star rating for the assistant w.r.t those requests.
	 */
	public Double getAvgStarRating(Long personId, List<LabRequest> requests) {
		List<Integer> ratings = countRatings(personId, requests);
		double totalRatings = 0.0;
		double totalCounts = 0.0;
		for (int i = 0; i < ratings.size(); i++) {
			totalRatings += ratings.get(i) * (i + 1);
			totalCounts += ratings.get(i);
		}
		if (totalCounts == 0.0)
			return 0.0;

		return new BigDecimal(totalRatings / totalCounts)
				.setScale(1, RoundingMode.HALF_UP)
				.doubleValue();
	}

	/**
	 * Updates feedback by the given request id and assistant id, but only if a feedback is already in the
	 * database by these ids. If none is found, a new feedback object is saved to the database and the given
	 * rating and feedback string are filled in it.
	 *
	 * @param request     The request the feedback is on.
	 * @param assistantId The id of the assistant the feedback is on.
	 * @param dto         The transferred Patch DTO representing the feedback.
	 */
	@Transactional
	public void updateFeedback(LabRequest request, Long assistantId, FeedbackPatchDTO dto) {
		fr.findById(request.getId(), assistantId)
				.map(f -> converter.apply(dto, f))
				.orElseGet(() -> {
					if ((dto.getFeedback() == null || dto.getFeedback().isEmpty())
							&& dto.getRating() == null) {
						throw new ValidationException("Cannot have empty feedback and empty rating.");
					}

					return fr.save(Feedback.builder()
							.id(new Feedback.Id(request.getId(), assistantId))
							.request(request)
							.feedback(dto.getFeedback())
							.rating(dto.getRating())
							.createdAt(now())
							.lastUpdatedAt(now())
							.build());
				});
	}

	/**
	 * Reports a feedback
	 *
	 * @param id The id of the feedback being reported.
	 */
	@Transactional
	public void reportFeedback(Feedback.Id id) {
		var feedback = fr.findById(id.getRequestId(), id.getAssistantId()).orElseThrow();
		feedback.setIsReported(true);
	}

	/**
	 * Filters feedback for a manager, such that they can only see feedback for TAs in courses that they
	 * managed.
	 *
	 * @param  feedback The list of feedback to be filtered.
	 * @return          A Page of feedback that the manager can see.
	 */
	public List<Feedback> filterFeedbackForManagerCourses(List<Feedback> feedback) {
		feedback = feedback.stream().filter(fb -> labRequestRepository.existsById(fb.getId().getRequestId()))
				.toList();

		List<Long> sessionIds = feedback.stream()
				.map(fb -> fb.getRequest().getSession().getSession())
				.distinct()
				.toList();

		Map<Long, Boolean> canManage = sessionService.getCoreSessions(sessionIds)
				.stream()
				.collect(Collectors.toMap(
						SessionDetailsDTO::getId,
						session -> ps
								.canManageInAnyEdition(new ArrayList<>(session.getEditions()))));

		return feedback.stream()
				.filter(fb -> canManage.getOrDefault(fb.getRequest().getSession().getSession(), false))
				.toList();
	}

	/**
	 * Filters the feedback and returns the feedback that has text
	 *
	 * @param  feedback The intial feedback list to be considered
	 * @return          A filtered list which contains feedback objects which have textual feedback.
	 */
	public List<Feedback> filterTextualFeedback(List<Feedback> feedback) {
		return feedback.stream()
				.filter(f -> f.getFeedback() != null && !f.getFeedback().isBlank())
				.toList();
	}

	public FeedbackViewDTO convertToDTO(Feedback feedback) {
		FeedbackViewDTO view = converter.convert(feedback, FeedbackViewDTO.class);

		view.setRequestExists(labRequestRepository.existsById(view.getId().getRequestId()));

		// If the request doesn't exist, but the feedback does, we cannot fetch the following data since it doesn't exist in core.
		if (view.isRequestExists()) {
			var studentGroup = groupCache.getRequired(feedback.getRequest().getStudentGroup());
			if (studentGroup.getMemberUsernames().size() == 1) {
				view.setGroupName(studentGroup.getMembers().get(0).getPerson().getDisplayName());
			} else {
				view.setGroupName(studentGroupName(studentGroup));
			}

			SessionDetailsDTO session = sessionCache
					.getRequired(feedback.getRequest().getSession().getSession());
			view.setEditionName(session.getEdition() == null ? session.getEditionCollection().getName()
					: session.getEdition().getName());
		}

		return view;
	}

	/**
	 * Gets the name of the student group that this request is from.
	 *
	 * @return The name of the group that the request was created for with description of its members.
	 */
	private String studentGroupName(StudentGroupDetailsDTO studentGroup) {
		return "Group " + studentGroup.getName() + " (" +
				Strings.join(", ", studentGroup.getMembers().stream()
						.filter(r -> r.getType() == RolePersonLayer1DTO.TypeEnum.STUDENT)
						.map(r -> r.getPerson().getDisplayName()).collect(Collectors.toList()))
				+
				")";
	}

}
