ChallengeServiceImpl.java

package no.ntnu.idi.stud.savingsapp.service.impl;

import no.ntnu.idi.stud.savingsapp.exception.goal.ChallengeNotFoundException;
import no.ntnu.idi.stud.savingsapp.exception.goal.GoalNotFoundException;
import no.ntnu.idi.stud.savingsapp.exception.goal.InvalidChallengeDayException;
import no.ntnu.idi.stud.savingsapp.exception.user.PermissionDeniedException;
import no.ntnu.idi.stud.savingsapp.exception.user.UserNotFoundException;
import no.ntnu.idi.stud.savingsapp.model.configuration.ChallengeType;
import no.ntnu.idi.stud.savingsapp.model.goal.Challenge;
import no.ntnu.idi.stud.savingsapp.model.goal.ChallengeTemplate;
import no.ntnu.idi.stud.savingsapp.model.goal.Goal;
import no.ntnu.idi.stud.savingsapp.model.goal.Progress;
import no.ntnu.idi.stud.savingsapp.model.user.User;
import no.ntnu.idi.stud.savingsapp.repository.ChallengeTemplateRepository;
import no.ntnu.idi.stud.savingsapp.repository.GoalRepository;
import no.ntnu.idi.stud.savingsapp.repository.UserRepository;
import no.ntnu.idi.stud.savingsapp.service.ChallengeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.sql.Timestamp;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.*;

/**
 * Implementation of ChallengeService to manage challenges within goals.
 */
@Service
public class ChallengeServiceImpl implements ChallengeService {

	private static final Random random = new Random();

	@Autowired
	private GoalRepository goalRepository;

	@Autowired
	private ChallengeTemplateRepository challengeTemplateRepository;

	@Autowired
	private UserRepository userRepository;

	/**
	 * Generates a list of challenges for a given goal based on the user's preferences and
	 * goal's target date. Each challenge is generated based on predefined templates and
	 * adjusted for the duration of the goal.
	 * @param goal The goal for which to generate challenges.
	 * @param user The user who owns the goal.
	 * @return A list of generated Challenge objects.
	 */
	@Override
	public List<Challenge> generateChallenges(Goal goal, User user) {
		ChallengeTemplate t1 = new ChallengeTemplate();
		t1.setChallengeType(ChallengeType.NO_COFFEE);
		t1.setAmount(BigDecimal.valueOf(40));
		t1.setText("Spar {unit_amount} kr hver gang du kjøper kaffe, totalt {checkDays} ganger over "
				+ "{totalDays} dager. Dette gir deg en total besparelse på {total_amount} kr.");
		t1.setChallengeName("Spar på kaffe");
		ChallengeTemplate t2 = new ChallengeTemplate();
		t2.setChallengeType(ChallengeType.EAT_PACKED_LUNCH);
		t2.setAmount(BigDecimal.valueOf(30));
		t2.setText("Spar {unit_amount} kr per gang du tar med deg hjemmelaget matpakke, "
				+ "totalt {checkDays} ganger over "
				+ "{totalDays} dager. Dette gir deg en total besparelse på {total_amount} kr.");
		t2.setChallengeName("Spar på mat");

		List<ChallengeTemplate> templates = Arrays.asList(t1, t2);
		// templateRepository.findAllByChallengeTypeIn(user.getConfiguration().getChallengeTypes());
		Collections.shuffle(templates);

		LocalDateTime targetDate = goal.getTargetDate().toLocalDateTime();
		int remainingDays = (int) ChronoUnit.DAYS.between(LocalDate.now(), targetDate);

		List<Challenge> challenges = new ArrayList<>();
		int i = 0;

		int savedSoFar = 0;

		while (remainingDays > 0) {
			int totalDays = Math.min(random.nextInt(23) + 7, remainingDays);
			int checkDays = user.getConfiguration().getCommitment().getCheckDays(totalDays);
			ChallengeTemplate template = templates.get(i++ % templates.size());
			Challenge challenge = new Challenge();
			challenge.setTemplate(template);
			challenge.setPotentialAmount(template.getAmount());
			challenge.setPoints(checkDays * 10);
			challenge.setCheckDays(checkDays);
			challenge.setTotalDays(totalDays);

			savedSoFar += template.getAmount().intValue() * checkDays;

			if (challenges.isEmpty()) {
				challenge.setStartDate(goal.getCreatedAt());
			}
			else {
				Timestamp lastEndDate = challenges.get(challenges.size() - 1).getEndDate();
				LocalDate localDate = lastEndDate.toLocalDateTime().toLocalDate().plusDays(1);
				Timestamp timestamp = Timestamp.valueOf(localDate.atStartOfDay());
				challenge.setStartDate(timestamp);
			}
			Timestamp lastEndDate = challenge.getStartDate();
			LocalDate localDate = lastEndDate.toLocalDateTime().toLocalDate().plusDays(totalDays);
			Timestamp timestamp = Timestamp.valueOf(localDate.atStartOfDay());
			challenge.setEndDate(timestamp);

			if (challenge.getEndDate().after(goal.getTargetDate())) {
				break;
			}

			challenges.add(challenge);

			if (goal.getTargetAmount() < savedSoFar) {
				break;
			}

			remainingDays -= totalDays;
		}
		return challenges;
	}

	/**
	 * Updates the progress for a specific challenge on a specified day with a given
	 * amount. Validates user permissions, challenge existence, and the validity of the
	 * specified day.
	 * @param userId The ID of the user updating the challenge.
	 * @param id The ID of the challenge to update.
	 * @param day The day of the challenge to mark as completed.
	 * @param amount The amount saved or achieved on the specified day.
	 * @throws PermissionDeniedException if the user does not own the goal associated with
	 * the challenge.
	 * @throws ChallengeNotFoundException if the challenge cannot be found within the
	 * goal.
	 * @throws InvalidChallengeDayException if the specified day is invalid or already
	 * completed.
	 * @throws GoalNotFoundException if the goal associated with the challenge is not
	 * found.
	 */
	@Override
	public void updateProgress(long userId, long id, int day, BigDecimal amount) {
		Goal goal = getGoalByChallengeId(id);
		if (goal.getUser().getId() != userId) {
			throw new PermissionDeniedException();
		}

		Challenge challenge = goal.getChallenges().stream().filter(c -> c.getId() == id).findFirst().orElse(null);
		if (challenge == null) {
			throw new ChallengeNotFoundException();
		}
		List<Progress> progressList = challenge.getProgressList();

		if (progressList.stream().anyMatch(p -> p.getDay() == day)) {
			throw new InvalidChallengeDayException("Day is already completed");
		}
		if (day > challenge.getCheckDays() || day < 1) {
			throw new InvalidChallengeDayException("Day outside of range");
		}

		Progress progress = new Progress();
		progress.setDay(day);
		progress.setCompletedAt(Timestamp.from(Instant.now()));
		progress.setAmount(amount);
		progressList.add(progress);

		goalRepository.save(goal);
	}

	/**
	 * Updates the potential saving amount for a specific challenge within a goal. This
	 * method ensures that only the owner of the goal can update the challenge, and
	 * verifies that the challenge exists.
	 * @param userId The ID of the user attempting to update the saving amount.
	 * @param id The ID of the challenge whose saving amount is being updated.
	 * @param amount The new saving amount to be set for the challenge.
	 * @throws PermissionDeniedException if the user trying to update the saving amount
	 * does not own the goal.
	 * @throws ChallengeNotFoundException if no challenge with the given ID can be found
	 * within the goal.
	 * @throws GoalNotFoundException if no goal containing the specified challenge can be
	 * found.
	 */
	@Override
	public void updateSavingAmount(long userId, long id, BigDecimal amount) {
		Goal goal = getGoalByChallengeId(id);
		if (goal.getUser().getId() != userId) {
			throw new PermissionDeniedException();
		}

		Challenge challenge = goal.getChallenges().stream().filter(c -> c.getId() == id).findFirst().orElse(null);
		if (challenge == null) {
			throw new ChallengeNotFoundException();
		}
		challenge.setPotentialAmount(amount);

		goalRepository.save(goal);
	}

	/**
	 * Retrieves a goal that contains a specific challenge identified by the challenge ID.
	 * This method is useful for operations requiring access to a goal based on one of its
	 * challenges, ensuring the challenge's existence within the goal structure.
	 * @param challengeId The ID of the challenge whose goal is to be retrieved.
	 * @return The Goal containing the specified challenge.
	 * @throws GoalNotFoundException If no goal containing the specified challenge can be
	 * found.
	 */
	private Goal getGoalByChallengeId(long challengeId) {
		Optional<Goal> goalOptional = goalRepository.findByChallenges_Id(challengeId);
		if (goalOptional.isPresent()) {
			return goalOptional.get();
		}
		else {
			throw new ChallengeNotFoundException();
		}
	}

	/**
	 * Replaces that challenge in a given goal identified by the challenge ID. This method
	 * is useful for letting the user change out challenges they know they will not be
	 * able to do
	 * @param userId the ID of the user who sends the request
	 * @param challengeId The ID of the challenge that will be replaced
	 * @return The updated goal containing the new challenge
	 */
	public Challenge regenerateChallenge(long userId, long challengeId) {
		Optional<User> userOptional = userRepository.findById(userId);
		if (userOptional.isPresent()) {
			User user = userOptional.get();

			Goal goal = getGoalByChallengeId(challengeId);
			if (goal.getUser().getId() != userId) {
				throw new PermissionDeniedException();
			}

			Challenge challenge = goal.getChallenges()
				.stream()
				.filter(c -> c.getId() == challengeId)
				.findFirst()
				.orElse(null);
			if (challenge == null) {
				throw new ChallengeNotFoundException();
			}

			List<ChallengeType> challengeTypes = user.getConfiguration().getChallengeTypes();
			List<ChallengeTemplate> challengeTemplates = challengeTemplateRepository
				.findAllByChallengeTypeIn(challengeTypes);

			int randomTemplateIndex = random.nextInt(challengeTemplates.size());

			ChallengeTemplate newChallengeTemplate = challengeTemplates.get(randomTemplateIndex);

			int totalDays = challenge.getTotalDays();
			int checkDays = user.getConfiguration().getCommitment().getCheckDays(totalDays);

			challenge.setPotentialAmount(newChallengeTemplate.getAmount());
			challenge.setCheckDays(checkDays);
			challenge.setPoints(checkDays * 10);
			challenge.setTemplate(newChallengeTemplate);

			goalRepository.save(goal);
			return challenge;
		}
		else {
			throw new UserNotFoundException();
		}
	}

}