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

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

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import jakarta.transaction.Transactional;
import nl.tudelft.labracore.api.AssignmentControllerApi;
import nl.tudelft.labracore.api.PersonControllerApi;
import nl.tudelft.labracore.api.dto.PersonSummaryDTO;
import nl.tudelft.labracore.api.dto.RoomDetailsDTO;
import nl.tudelft.labracore.lib.security.user.AuthenticatedPerson;
import nl.tudelft.labracore.lib.security.user.Person;
import nl.tudelft.librador.dto.DTOConverter;
import nl.tudelft.librador.resolver.annotations.PathEntity;
import nl.tudelft.queue.cache.*;
import nl.tudelft.queue.dto.patch.FeedbackPatchDTO;
import nl.tudelft.queue.dto.patch.RequestPatchDTO;
import nl.tudelft.queue.dto.util.RequestTableFilterDTO;
import nl.tudelft.queue.model.LabRequest;
import nl.tudelft.queue.model.QueueSession;
import nl.tudelft.queue.model.Request;
import nl.tudelft.queue.model.SelectionRequest;
import nl.tudelft.queue.model.enums.RequestStatus;
import nl.tudelft.queue.model.labs.Lab;
import nl.tudelft.queue.repository.LabRequestRepository;
import nl.tudelft.queue.repository.ProfileRepository;
import nl.tudelft.queue.service.*;
import nl.tudelft.queue.service.dto.RequestViewDTOService;

@Controller
public class RequestController {

	@Autowired
	private LabRequestRepository lrr;

	@Autowired
	private ProfileRepository pr;

	@Autowired
	private RequestService rs;

	@Autowired
	private LabService ls;

	@Autowired
	private RequestTableService rts;

	@Autowired
	private PersonControllerApi pApi;

	@Autowired
	private AssignmentControllerApi aApi;

	@Autowired
	private RoleDTOService rds;

	@Autowired
	private FeedbackService fs;

	@Autowired
	private RequestViewDTOService requestViewDTOService;

	@Autowired
	private SessionService sessionService;

	@Autowired
	private EditionCacheManager eCache;

	@Autowired
	private ModuleCacheManager mCache;

	@Autowired
	private AssignmentCacheManager aCache;

	@Autowired
	private PersonCacheManager pCache;

	@Autowired
	private SessionCacheManager sCache;

	@Autowired
	private DTOConverter converter;

	/**
	 * Sets a model attribute statically within every Thymeleaf resolution. This model attribute is to
	 * indicate the main tab that requests pages are in.
	 *
	 * @return The name of the tab that these pages are on ("requests").
	 */
	@ModelAttribute("page")
	public static String page() {
		return "requests";
	}

	/**
	 * Gets the view for the next request or a redirect to the requests page if no new request can be found.
	 *
	 * @param  lab    The lab that the person is requesting a new request for.
	 * @param  person The person that is requesting a new request to handle.
	 * @return        The redirect to either the request view or to the request table page.
	 */
	@GetMapping("/requests/next/{lab}")
	@PreAuthorize("@permissionService.canTakeRequest(#lab.id)")
	public String getNextRequest(@PathEntity Lab lab,
			@AuthenticatedPerson Person person) {
		RequestTableFilterDTO filter = rts.checkAndStoreFilterDTO(null, "/requests");
		Optional<LabRequest> request = rs.takeNextRequest(person, lab, filter);

		return request.map(r -> "redirect:/request/" + r.getId()).orElse("redirect:/requests");
	}

	/**
	 * Gets the request table view page. This page should be accessible to assistants, TAs, teachers, etc.,
	 * but not for regular students. This page displays a big table containing all currently open requests.
	 * The table can be filtered to display less requests and a next request can be grabbed from this page.
	 *
	 * @param  pageable The pageable determining the current size and page of the view.
	 * @param  model    The model to fill out for Thymeleaf template resolution.
	 * @return          The Thymeleaf template to resolve.
	 */
	@GetMapping("/requests")
	@PreAuthorize("@permissionService.isStaffInAnyEdition()")
	public String getRequestTableView(Model model,
			@AuthenticatedPerson Person assistant,
			@PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC, size = 25) Pageable pageable) {
		model.addAttribute("showName", false);
		model.addAttribute("showOnlyRelevant", true);
		setRequestTableAttributes(model, pageable, assistant,
				"/requests",
				r -> r.getEventInfo().getStatus() == RequestStatus.PENDING ||
						(r.getEventInfo().getStatus() == RequestStatus.FORWARDED &&
								!Objects.equals(r.getEventInfo().getForwardedBy(), assistant.getId()) &&
								(r.getEventInfo().getForwardedTo() == null ||
										r.getEventInfo().getForwardedTo().equals(assistant.getId()))),
				false, true);
		return "request/list";
	}

	/**
	 * Gets the request table history view page. This page should be accessible to assistants, TAs, teachers,
	 * etc., but not for regular students. This page displays a big table containing all current requests.
	 *
	 * @param  pageable The pageable determining the current size and page of the view.
	 * @param  model    The model to fill out for Thymeleaf template resolution.
	 * @return          The Thymeleaf template to resolve.
	 */
	@GetMapping("/requests/history")
	@PreAuthorize("@permissionService.isStaffInAnyEdition()")
	public String getRequestTableHistoryView(Model model,
			@AuthenticatedPerson Person assistant,
			@PageableDefault(sort = "createdAt", direction = Sort.Direction.DESC, size = 25) Pageable pageable) {
		model.addAttribute("showName", true);
		model.addAttribute("showOnlyRelevant", false);
		setRequestTableAttributes(model, pageable, assistant,
				"/requests/history", r -> !r.getEventInfo().getStatus().isPending(), true, false);
		return "request/list";
	}

	private void setRequestTableAttributes(Model model, Pageable pageable, Person assistant,
			String filterPath,
			Predicate<LabRequest> requestFilter,
			boolean reversed, boolean forwardedFirst) {
		var filter = rts.checkAndStoreFilterDTO(null, filterPath);
		var language = pr.findProfileForPerson(assistant).getLanguage();

		List<QueueSession<?>> qSessions = rts.addFilterAttributes(model, null);
		List<Lab> labs = qSessions.stream()
				.filter(qs -> qs instanceof Lab)
				.map(qs -> (Lab) qs)
				.collect(Collectors.toList());

		List<LabRequest> filteredRequests = rs
				.filterRequestsSharedEditionCheck(lrr.findAllByFilterUpcoming(labs, filter, language))
				.stream().filter(requestFilter)
				.toList();

		model.addAttribute("page", "requests");
		model.addAttribute("filter", filter);
		model.addAttribute("requests",
				rts.convertRequestsToView(filteredRequests, pageable, reversed, forwardedFirst));
		model.addAttribute("requestCounts", rts.labRequestCounts(
				labs, assistant, filter));
	}

	/**
	 * Handles a post to unpartake in the event.
	 *
	 * @return A redirect back to the requests page, but without the event.
	 */
	@PostMapping("/requests/ilikemyeyesthankyou")
	@PreAuthorize("@permissionService.isStaffInAnyEdition()")
	public String unpartakeInEvent() {
		rts.updatePartakingStatus(false);

		return "redirect:/requests";
	}

	/**
	 * Handles a post to partake in the event.
	 *
	 * @return A redirect back to the requests page, but the event.
	 */
	@PostMapping("/requests/iloveallqueuedevelopers")
	@PreAuthorize("@permissionService.isStaffInAnyEdition()")
	public String partakeInEvent() {
		rts.updatePartakingStatus(true);

		return "redirect:/requests";
	}

	/**
	 * Updates the information on the location of a student within the given request.
	 *
	 * @param  request The request to update location information on.
	 * @param  dto     The dto carrying the update information on the location.
	 * @return         A redirect back to the lab view page.
	 */
	@Transactional
	@PostMapping("/request/{request}/update-request-info")
	@PreAuthorize("@permissionService.canUpdateRequest(#request.id)")
	public String updateRequestInfo(@PathEntity LabRequest request, RequestPatchDTO dto) {
		dto.validateRooms(sessionService.getCoreSession(request.getSession().getSession()).getRooms().stream()
				.map(RoomDetailsDTO::getId).toList());
		dto.apply(request, converter);

		return "redirect:/lab/" + request.getSession().getId();
	}

	/**
	 * Gets the singular request view page. This page should be accessible to all those
	 *
	 * @param  request The request that is requested to be viewed.
	 * @param  model   The model to fill out for Thymeleaf template resolution.
	 * @return         The Thymeleaf template to resolve.
	 */
	@GetMapping("/request/{request}")
	@PreAuthorize("@permissionService.canViewRequest(#request.id)")
	public String getRequestView(@PathEntity Request<?> request, Model model) {
		if (request.getEventInfo().getAssignedTo() != null) {
			model.addAttribute("assistant", pApi
					.getPersonById(request.getEventInfo().getAssignedTo()).block());
		}

		ls.setOrganizationInModel(request.getSession(), model);
		model.addAttribute("request", requestViewDTOService.convert(request));

		if (request instanceof LabRequest) {
			model.addAttribute("prevRequests",
					requestViewDTOService.convert(lrr.findAllPreviousRequests((LabRequest) request)));

			return "request/view/lab";
		} else {
			return "request/view/selection";
		}
	}

	/**
	 * Gets the request approval view. This page is used to submit a request approval.
	 *
	 * @param  request The request that is requested to be approved.
	 * @param  model   The model to fill out for Thymeleaf template resolution.
	 * @return         The Thymeleaf template to resolve.
	 */
	@GetMapping("/request/{request}/approve")
	@PreAuthorize("@permissionService.canFinishRequest(#request.id)")
	public String getRequestApproveView(@PathEntity LabRequest request, Model model) {
		model.addAttribute("request", request);
		ls.setOrganizationInModel(request.getSession(), model);

		return "request/approve";
	}

	/**
	 * Performs the 'approve request' action for a request. This controller method calls the service method to
	 * handle a request approve command and redirects the user back to the requests page to grab their next
	 * request.
	 *
	 * @param  request            The request that the user approves.
	 * @param  person             The user that is approving the given request.
	 * @param  reasonForAssistant The reason the assistant gives other assistants for approving the request.
	 * @param  redirectAttributes Attributes used to fill in a model message upon redirection.
	 * @return                    A redirect to the requests table page.
	 */
	@PostMapping("/request/{request}/approve")
	@PreAuthorize("@permissionService.canFinishRequest(#request.id)")
	public String approveRequest(@PathEntity LabRequest request,
			@AuthenticatedPerson Person person,
			@RequestParam(value = "reasonForAssistant") String reasonForAssistant,
			@RequestParam(value = "answer", required = false) String answer,
			RedirectAttributes redirectAttributes) {
		rs.approveRequest(request, person.getId(), reasonForAssistant, answer);

		redirectAttributes.addFlashAttribute("message", "Request #" + request.getId() + " approved");

		return "redirect:/requests";
	}

	/**
	 * Gets the request reject view. This page is used to submit a request rejection.
	 *
	 * @param  request The request that is requested to be rejected.
	 * @param  model   The model to fill out for Thymeleaf template resolution.
	 * @return         The Thymeleaf template to resolve.
	 */
	@GetMapping("/request/{request}/reject")
	@PreAuthorize("@permissionService.canFinishRequest(#request.id)")
	public String getRequestRejectView(@PathEntity Request<?> request, Model model) {
		model.addAttribute("request", request);
		ls.setOrganizationInModel(request.getSession(), model);

		return "request/reject";
	}

	/**
	 * Performs the 'reject request' action for a request. This controller method calls the service method to
	 * handle a request reject command and redirects the user back to the requests page to grab their next
	 * request.
	 *
	 * @param  request            The request that the user rejects.
	 * @param  person             The user that is rejecting the given request.
	 * @param  reasonForAssistant The reason the assistant gives other assistants for rejecting the request.
	 * @param  reasonForStudent   The reason the assistant gives the student(s) for rejecting the request.
	 * @param  redirectAttributes Attributes used to fill in a model message upon redirection.
	 * @return                    A redirect to the requests table page.
	 */
	@PostMapping("/request/{request}/reject")
	@PreAuthorize("@permissionService.canFinishRequest(#request.id)")
	public String rejectRequest(@PathEntity Request<?> request,
			@AuthenticatedPerson Person person,
			@RequestParam(value = "reasonForAssistant") String reasonForAssistant,
			@RequestParam(value = "reasonForStudent") String reasonForStudent,
			RedirectAttributes redirectAttributes) {
		if (request instanceof LabRequest) {
			rs.rejectRequest((LabRequest) request, person.getId(), reasonForAssistant, reasonForStudent);
		} else {
			rs.rejectSelectionRequest((SelectionRequest) request, person.getId(),
					reasonForAssistant, reasonForStudent);
		}

		redirectAttributes.addFlashAttribute("message", "Request #" + request.getId() + " rejected");

		return "redirect:/requests";
	}

	/**
	 * Gets the request forward view. This page is used to forward a request to a different TA.
	 *
	 * @param  request The request that is requested to be forwarded.
	 * @param  model   The model to fill out for Thymeleaf template resolution.
	 * @return         The Thymeleaf template to resolve.
	 */
	@GetMapping("/request/{request}/forward")
	@PreAuthorize("@permissionService.canFinishRequest(#request.id)")
	public String getRequestForwardView(@AuthenticatedPerson Person user,
			@PathEntity LabRequest request, Model model) {

		List<PersonSummaryDTO> assistants = rds
				.roles(eCache.getRequired(
						mCache.getRequired(aCache.getRequired(request.getAssignment()).getModule().getId())
								.getEdition().getId()),
						Set.of(TA, HEAD_TA, TEACHER))
				.stream().filter(p -> !p.getId().equals(user.getId())).toList();

		ls.setOrganizationInModel(request.getSession(), model);

		model.addAttribute("request", request);
		model.addAttribute("assistants", assistants);

		return "request/forward";
	}

	/**
	 * Performs the 'forward request' action for a request. This controller method calls the service method to
	 * handle a request forward command and redirects the user back to the requests page to grab their next
	 * request.
	 *
	 * @param  request            The request that the user forwards.
	 * @param  assistant          The assistant the request should be forwarded to.
	 * @param  reasonForAssistant The reason the assistant gives other assistants for forwarding the request.
	 * @param  redirectAttributes Attributes used to fill in a model message upon redirection.
	 * @return                    A redirect to the requests table page.
	 */
	@PostMapping("/request/{request}/forward")
	@PreAuthorize("@permissionService.canFinishRequest(#request.id)")
	public String forwardRequest(@PathEntity LabRequest request,
			@AuthenticatedPerson Person assistant,
			@RequestParam(value = "assistant") Long forwardedTo,
			@RequestParam(value = "reasonForAssistant") String reasonForAssistant,
			RedirectAttributes redirectAttributes) {
		if (forwardedTo == -1L) {
			rs.forwardRequestToAnyone(request, assistant, reasonForAssistant);
		} else {
			rs.forwardRequestToPerson(request, assistant, pCache.getRequired(forwardedTo),
					reasonForAssistant);
		}

		redirectAttributes.addFlashAttribute("message", "Request #" + request.getId() + " forwarded");

		return "redirect:/requests";
	}

	/**
	 * Makes a request not found status request for the specific request. This means the final request status
	 * will be changed to "Not Found" and the request will be labeled as such everywhere it can be seen.
	 *
	 * @param  request The request to mark "Not Found".
	 * @return         A redirect to the requests table page.
	 */
	@GetMapping("/request/{request}/not-found")
	@PreAuthorize("@permissionService.canFinishRequest(#request.id)")
	public String couldNotFindStudent(@PathEntity LabRequest request,
			@AuthenticatedPerson Person person) {
		rs.couldNotFindStudent(request, person);

		return "redirect:/requests";
	}

	/**
	 * * Performs the 'revoke request' command and redirects the user back to the lab overview page, if the
	 * person who sent * the revoke request is the student themself. If someone else is doing it, it means
	 * it's a staff member trying to free up a space in a slotted lab, hence we redirect to the request
	 * itself, just so they can get confirmation.
	 *
	 * @param  revoker The person who sends in the revoke request. It can be the student themself or a staff
	 *                 member.
	 * @param  request The request to revoke.
	 * @return         A redirect based on the person who is requesting the revocation.
	 */
	@Transactional
	@PostMapping("/request/{request}/revoke")
	@PreAuthorize("@permissionService.canRevokeRequest(#request)")
	public String revokeRequest(@AuthenticatedPerson Person revoker,
			@PathEntity Request<?> request) {
		rs.revokeRequest(request, revoker.getId());

		if (Objects.equals(revoker.getId(), request.getRequester())) {
			return "redirect:/lab/" + request.getSession().getId();
		} else {
			return "redirect:/requests";
		}
	}

	/**
	 * Allows teachers to choose a specific request to handle which is not yet being processed by a TA to
	 * allow out of order processing.
	 *
	 * @param  request The request the user wants to pick
	 * @param  person  The person who wants to pick the request
	 * @return         The request the user is currently processing. Either the picked request, or the request
	 *                 he/she was still processing.
	 */
	@GetMapping("/request/{request}/pick")
	@PreAuthorize("@permissionService.canPickRequest(#request.id)")
	public String pickRequest(@PathEntity LabRequest request, @AuthenticatedPerson Person person) {
		LabRequest currRequest = rs.pickRequest(person, request);
		return "redirect:/request/" + currRequest.getId();
	}

	/**
	 * Creates or changes the feedback of a group on a TA on the given request.
	 *
	 * @param  request     The request on which the feedback is placed.
	 * @param  assistantId The id of the assistant on which the feedback is placed.
	 * @return             A redirect back to the same request overview.
	 */
	@PostMapping("/request/{request}/feedback/{assistantId}")
	@PreAuthorize("@permissionService.canGiveFeedback(#request.id, #assistantId)")
	public String giveFeedback(@PathEntity LabRequest request,
			@PathVariable Long assistantId,
			FeedbackPatchDTO dto) {
		fs.updateFeedback(request, assistantId, dto);

		return "redirect:/request/" + request.getId();
	}

}
