content: package com.nonononoki.alovoa.service;
  
  import com.fasterxml.jackson.core.JsonProcessingException;
  import com.fasterxml.jackson.databind.ObjectMapper;
  import com.linkedin.urls.Url;
  import com.linkedin.urls.detection.UrlDetector;
  import com.linkedin.urls.detection.UrlDetectorOptions;
  import com.nonononoki.alovoa.Tools;
  import com.nonononoki.alovoa.component.ExceptionHandler;
  import com.nonononoki.alovoa.component.TextEncryptorConverter;
  import com.nonononoki.alovoa.entity.User;
  import com.nonononoki.alovoa.entity.user.*;
  import com.nonononoki.alovoa.lib.OxCaptcha;
  import com.nonononoki.alovoa.model.*;
  import com.nonononoki.alovoa.repo.*;
  import com.sipgate.mp3wav.Converter;
  import jakarta.mail.MessagingException;
  import lombok.Getter;
  import org.apache.commons.lang3.RandomStringUtils;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.beans.factory.annotation.Value;
  import org.springframework.context.i18n.LocaleContextHolder;
  import org.springframework.core.io.ByteArrayResource;
  import org.springframework.core.io.Resource;
  import org.springframework.data.domain.PageRequest;
  import org.springframework.http.*;
  import org.springframework.security.core.context.SecurityContextHolder;
  import org.springframework.stereotype.Service;
  
  import javax.crypto.BadPaddingException;
  import javax.crypto.IllegalBlockSizeException;
  import javax.crypto.NoSuchPaddingException;
  import javax.imageio.ImageIO;
  import javax.sound.sampled.*;
  import java.awt.*;
  import java.awt.image.BufferedImage;
  import java.io.ByteArrayInputStream;
  import java.io.ByteArrayOutputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.net.URLDecoder;
  import java.nio.charset.StandardCharsets;
  import java.security.InvalidAlgorithmParameterException;
  import java.security.InvalidKeyException;
  import java.security.NoSuchAlgorithmException;
  import java.security.SecureRandom;
  import java.text.DecimalFormat;
  import java.text.DecimalFormatSymbols;
  import java.util.List;
  import java.util.*;
  import java.util.function.Predicate;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  import java.util.stream.Collectors;
  
  @Service
  public class UserService {
  
      private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class);
      private static final int verificationStringLength = 5;
      private static final String MIME_X_WAV = "x-wav";
      private static final String MIME_WAV = "wav";
      private static final String MIME_MPEG = "mpeg";
      private static final String MIME_MP3 = "mp3";
  
      private final Set<Long> drugsAlcoholMiscInfoSet = new HashSet<>(Set.of(UserMiscInfo.DRUGS_ALCOHOL_YES, UserMiscInfo.DRUGS_ALCOHOL_SOMETIMES, UserMiscInfo.DRUGS_ALCOHOL_NO));
      private final Set<Long> drugsTobaccoMiscInfoSet = new HashSet<>(Set.of(UserMiscInfo.DRUGS_TOBACCO_YES, UserMiscInfo.DRUGS_TOBACCO_SOMETIMES, UserMiscInfo.DRUGS_TOBACCO_NO));
      private final Set<Long> drugsCannabisMiscInfoSet = new HashSet<>(Set.of(UserMiscInfo.DRUGS_CANNABIS_YES, UserMiscInfo.DRUGS_CANNABIS_SOMETIMES, UserMiscInfo.DRUGS_CANNABIS_NO));
      private final Set<Long> drugsOtherMiscInfoSet = new HashSet<>(Set.of(UserMiscInfo.DRUGS_OTHER_YES, UserMiscInfo.DRUGS_OTHER_SOMETIMES, UserMiscInfo.DRUGS_OTHER_NO));
  
      private final Set<Long> kidsMiscInfoSet = new HashSet<>(Set.of(UserMiscInfo.KIDS_NO, UserMiscInfo.KIDS_YES));
      private final Set<Long> relationshipMiscInfoSet = new HashSet<>(Set.of(UserMiscInfo.RELATIONSHIP_SINGLE, UserMiscInfo.RELATIONSHIP_TAKEN, UserMiscInfo.RELATIONSHIP_OPEN));
      private final Set<Long> politicsMiscInfoSet = new HashSet<>(Set.of(UserMiscInfo.POLITICS_MODERATE, UserMiscInfo.POLITICS_LEFT, UserMiscInfo.POLITICS_RIGHT));
      private final Set<Long> genderIdentityMiscInfoSet = new HashSet<>(Set.of(UserMiscInfo.GENDER_IDENTITY_CIS, UserMiscInfo.GENDER_IDENTITY_TRANS));
      private final Set<Long> religionMiscInfoSet = new HashSet<>(Set.of(UserMiscInfo.RELIGION_NO, UserMiscInfo.RELIGION_YES));
      private final Set<Long> familyMiscInfoSet = new HashSet<>(Set.of(UserMiscInfo.FAMILY_WANT, UserMiscInfo.FAMILY_NOT_WANT, UserMiscInfo.FAMILY_NOT_UNSURE));
      private final Set<Long> relationshipTypeMiscInfoSet = new HashSet<>(Set.of(UserMiscInfo.RELATIONSHIP_TYPE_POLYAMOROUS, UserMiscInfo.RELATIONSHIP_TYPE_MONOGAMOUS));
  
      @Autowired
      private AuthService authService;
      @Autowired
      private MediaService mediaService;
      @Autowired
      private UserRepository userRepo;
      @Autowired
      private GenderRepository genderRepo;
      @Autowired
      private UserMiscInfoRepository userMiscInfoRepo;
      @Autowired
      private UserIntentionRepository userIntentionRepo;
      @Autowired
      private UserInterestRepository userInterestRepo;
      @Autowired
      private UserImageRepository userImageRepo;
      @Autowired
      private UserLikeRepository userLikeRepo;
      @Autowired
      private UserHideRepository userHideRepo;
      @Autowired
      private UserBlockRepository userBlockRepo;
      @Autowired
      private UserReportRepository userReportRepo;
      @Autowired
      private UserNotificationRepository userNotificationRepo;
      @Autowired
      private UserProfilePictureRepository userProfilePictureRepository;
      @Autowired
      private UserVerificationPictureRepository userVerificationPictureRepo;
      @Autowired
      private ConversationRepository conversationRepo;
      @Autowired
      private CaptchaService captchaService;
      @Autowired
      private MailService mailService;
      @Autowired
      private TextEncryptorConverter textEncryptor;
      @Autowired
      private ObjectMapper objectMapper;
      @Value("${app.age.min}")
      private int minAge;
      @Value("${app.age.max}")
      private int maxAge;
      @Value("${app.profile.image.length}")
      private int imageLength;
      @Value("${app.profile.image.max}")
      private int imageMax;
      @Value("${app.profile.description.size}")
      private int descriptionSize;
      @Value("${app.token.length}")
      private int tokenLength;
      @Value("${app.interest.max}")
      private int interestSize;
      @Value("${app.interest.min-chars}")
      private int interestMinCharSize;
      @Value("${app.interest.max-chars}")
      private int interestMaxCharSize;
      @Value("${app.interest.autocomplete.max}")
      private int interestAutocompleteMax;
      @Value("${app.audio.max-time}")
      private int audioMaxTime; // in seconds
      @Value("${app.user.delete.duration.valid}")
      private long userDeleteDuration;
      @Value("${app.intention.delay}")
      private long intentionDelay;
      @Value("${app.like.message.length}")
      private int likeMessageLength;
      @Value("${app.search.ignore-intention}")
      private boolean ignoreIntention;
      @Getter
      @Value("${app.domain}")
      public String domain;
  
  
      public static synchronized void removeUserDataCascading(User user, UserDeleteParams userDeleteParam) {
  
          UserRepository userRepo = userDeleteParam.getUserRepo();
          UserLikeRepository userLikeRepo = userDeleteParam.getUserLikeRepo();
          ConversationRepository conversationRepo = userDeleteParam.getConversationRepo();
          UserNotificationRepository userNotificationRepo = userDeleteParam.getUserNotificationRepo();
          UserHideRepository userHideRepo = userDeleteParam.getUserHideRepo();
          UserBlockRepository userBlockRepo = userDeleteParam.getUserBlockRepo();
          UserReportRepository userReportRepo = userDeleteParam.getUserReportRepo();
          UserVerificationPictureRepository userVerificationPictureRepo = userDeleteParam.getUserVerificationPictureRepo();
  
          // DELETE USER LIKE
          userLikeRepo.deleteAll(userLikeRepo.findByUserFrom(user));
          List<UserLike> likeTos = userLikeRepo.findByUserTo(user);
          userLikeRepo.deleteAll(likeTos);
          userLikeRepo.flush();
          for (UserLike like : likeTos) {
              User u = like.getUserFrom();
              if (u != null && u.getLikes() != null) {
                  u.getLikes().remove(like);
                  userRepo.save(u);
              }
          }
          userRepo.flush();
  
          // DELETE USER NOTIFICATION
          userNotificationRepo.deleteAll(userNotificationRepo.findByUserFrom(user));
          List<UserNotification> notificationTos = userNotificationRepo.findByUserTo(user);
          userNotificationRepo.deleteAll(notificationTos);
          userNotificationRepo.flush();
          for (UserNotification notification : notificationTos) {
              User u = notification.getUserFrom();
              if (u != null && u.getNotifications() != null) {
                  u.getNotifications().remove(notification);
                  userRepo.save(u);
              }
          }
          userRepo.flush();
  
          // DELETE USER HIDE
          userHideRepo.deleteAll(userHideRepo.findByUserFrom(user));
          List<UserHide> hideTos = userHideRepo.findByUserTo(user);
          userHideRepo.deleteAll(hideTos);
          userHideRepo.flush();
          for (UserHide hide : hideTos) {
              User u = hide.getUserFrom();
              if (u != null && u.getHiddenUsers() != null) {
                  u.getHiddenUsers().remove(hide);
                  userRepo.save(u);
              }
          }
          userRepo.flush();
  
          // DELETE USER BLOCK
          userBlockRepo.deleteAll(userBlockRepo.findByUserFrom(user));
          userBlockRepo.deleteAll(userBlockRepo.findByUserTo(user));
          userBlockRepo.flush();
  
          // DELETE USER REPORT
          userReportRepo.deleteAll(userReportRepo.findByUserFrom(user));
          List<UserReport> reportTos = userReportRepo.findByUserTo(user);
          userReportRepo.deleteAll(reportTos);
          userReportRepo.flush();
          for (UserReport report : reportTos) {
              User u = report.getUserFrom();
              if (u != null && u.getReported() != null) {
                  u.getReported().remove(report);
                  userRepo.save(u);
              }
          }
          userRepo.flush();
  
          // DELETE USER CONVERSATION
          for (Conversation c : conversationRepo.findByUsers_Id(user.getId())) {
              for (User u : c.getUsers()) {
                  if (u != null && u.getConversations() != null) {
                      u.getConversations().remove(c);
                      userRepo.save(u);
                  }
              }
              conversationRepo.delete(c);
          }
          conversationRepo.flush();
  
          // DELETE USER VERIFICATION
          for (UserVerificationPicture v : userVerificationPictureRepo.findByUserNo(user)) {
              v.getUserNo().remove(user);
              userVerificationPictureRepo.save(v);
          }
          for (UserVerificationPicture v : userVerificationPictureRepo.findByUserYes(user)) {
              v.getUserYes().remove(user);
              userVerificationPictureRepo.save(v);
          }
  
          userVerificationPictureRepo.flush();
          userRepo.flush();
      }
  
      public static String stripB64Type(String s) {
          if (s.contains(",")) {
              return s.split(",")[1];
          }
          return s;
      }
  
      private static byte[] convertAudioMp3Wav(byte[] bytes) {
          InputStream inputStream = new ByteArrayInputStream(bytes);
          ByteArrayOutputStream output = new ByteArrayOutputStream();
          final AudioFormat audioFormat = new AudioFormat(16000, 8, 1, false, false);
          Converter.convertFrom(inputStream).withTargetFormat(audioFormat).to(output);
          return output.toByteArray();
      }
  
      public UserDeleteToken deleteAccountRequest() throws MessagingException, IOException, AlovoaException {
          User user = authService.getCurrentUser(true);
          return deleteAccountRequestBase(user);
      }
  
      public UserDeleteToken deleteAccountRequestBase(User user) throws MessagingException, IOException, AlovoaException {
          UserDeleteToken token = new UserDeleteToken();
          Date currentDate = new Date();
  
          token.setContent(RandomStringUtils.random(tokenLength, 0, 0, true, true, null, new SecureRandom()));
          token.setDate(currentDate);
          token.setUser(user);
          if (user.getDeleteToken() != null) {
              token.setId(user.getDeleteToken().getId());
          }
          user.setDeleteToken(token);
          user = userRepo.saveAndFlush(user);
  
          mailService.sendAccountDeleteRequest(user);
  
          return user.getDeleteToken();
      }
  
      public synchronized void deleteAccountConfirm(UserDeleteAccountDto dto)
              throws MessagingException, IOException, AlovoaException, NoSuchAlgorithmException {
          User user = userRepo.findByEmail(Tools.cleanEmail(dto.getEmail()));
  
          if (user == null) {
              throw new AlovoaException(ExceptionHandler.USER_NOT_FOUND);
          }
  
          UserDeleteToken deleteToken = user.getDeleteToken();
  
          if (!dto.isConfirm() || deleteToken == null) {
              throw new AlovoaException("deletion_not_confirmed");
          }
  
          String userTokenString = deleteToken.getContent();
  
          long ms = new Date().getTime();
          if (ms - user.getDeleteToken().getDate().getTime() > userDeleteDuration) {
              throw new AlovoaException("deletion_not_valid");
          }
  
          if (!dto.getTokenString().equals(userTokenString)) {
              LOGGER.debug("Expected:" + userTokenString + ". Got: " + dto.getTokenString());
              throw new AlovoaException("deletion_wrong_token");
          }
  
          if (!captchaService.isValid(dto.getCaptchaId(), dto.getCaptchaText())) {
              throw new AlovoaException("captcha_invalid");
          }
  
          UserDeleteParams userDeleteParam = UserDeleteParams.builder().conversationRepo(conversationRepo)
                  .userBlockRepo(userBlockRepo).userHideRepo(userHideRepo).userLikeRepo(userLikeRepo)
                  .userNotificationRepo(userNotificationRepo).userRepo(userRepo).userReportRepo(userReportRepo)
                  .userVerificationPictureRepo(userVerificationPictureRepo).build();
  
          removeUserDataCascading(user, userDeleteParam);
  
          user = userRepo.saveAndFlush(user);
          userRepo.delete(user);
          userRepo.flush();
  
          SecurityContextHolder.clearContext();
          mailService.sendAccountDeleteConfirm(user);
      }
  
      @SuppressWarnings("deprecation")
      public void updateProfilePicture(byte[] bytes, String mimeType) throws AlovoaException, IOException {
          User user = authService.getCurrentUser(true);
          user.setVerificationPicture(null);
          AbstractMap.SimpleEntry<byte[], String> adjustedImage = adjustPicture(bytes, mimeType);
          if (user.getProfilePicture() == null) {
              UserProfilePicture profilePic = new UserProfilePicture();
              profilePic.setBin(adjustedImage.getKey());
              profilePic.setBinMime(adjustedImage.getValue());
              profilePic.setUser(user);
              profilePic.setUuid(UUID.randomUUID());
              user.setProfilePicture(profilePic);
          } else {
              user.getProfilePicture().setBin(adjustedImage.getKey());
              user.getProfilePicture().setBinMime(adjustedImage.getValue());
              user.getProfilePicture().setData(null);
              user.getProfilePicture().setUuid(UUID.randomUUID());
          }
  
          userRepo.saveAndFlush(user);
      }
  
      public User onboarding(byte[] bytes, ProfileOnboardingDto model) throws AlovoaException, IOException {
          User user = authService.getCurrentUser(true);
          if (user.getProfilePicture() != null || user.getDescription() != null) {
              return null;
          }
  
          Date now = new Date();
  
          UserProfilePicture profilePic = new UserProfilePicture();
          AbstractMap.SimpleEntry<byte[], String> adjustedImage = adjustPicture(bytes, model.getProfilePictureMime());
          profilePic.setBin(adjustedImage.getKey());
          profilePic.setBinMime(adjustedImage.getValue());
          user.setProfilePicture(profilePic);
          user.setVerificationPicture(null);
  
          UserIntention intention = userIntentionRepo.findById(model.getIntention()).orElse(null);
          user.getDates().setIntentionChangeDate(now);
          user.setIntention(intention);
  
          List<Gender> preferredGenders = genderRepo.findAllById(model.getPreferredGenders());
          user.setPreferedGenders(new HashSet<>(preferredGenders));
  
          user.setDescription(model.getDescription());
          user.getInterests().addAll((model.getInterests().stream().map(i -> {
              UserInterest interest = new UserInterest();
              interest.setText(i.toLowerCase());
              interest.setUser(user);
              return interest;
          }).toList()));
  
          user.getUserSettings().setEmailLike(model.isNotificationLike());
          user.getUserSettings().setEmailChat(model.isNotificationChat());
  
          return userRepo.saveAndFlush(user);
      }
  
      public void updateDescription(String description) throws AlovoaException {
          if (description != null) {
              if (description.length() > descriptionSize) {
                  throw new AlovoaException("max_length_exceeded");
              }
              if (description.trim().isEmpty()) {
                  description = null;
              } else {
                  UrlDetector parser = new UrlDetector(description, UrlDetectorOptions.Default);
                  List<Url> urls = parser.detect();
                  if (!urls.isEmpty()) {
                      throw new AlovoaException("url_detected");
                  }
              }
          }
          User user = authService.getCurrentUser(true);
          user.setDescription(description);
          userRepo.saveAndFlush(user);
      }
  
      public void updateIntention(long intention) throws AlovoaException {
          User user = authService.getCurrentUser(true);
  
          Date now = new Date();
          if (user.getDates().getIntentionChangeDate() == null
                  || now.getTime() >= user.getDates().getIntentionChangeDate().getTime() + intentionDelay) {
              boolean isLegal = Tools.calcUserAge(user) >= Tools.AGE_LEGAL;
              if (!isLegal && intention == UserIntention.SEX) {
                  throw new AlovoaException("not_supported");
              }
              UserIntention i = userIntentionRepo.findById(intention).orElse(null);
              user.setIntention(i);
              user.getDates().setIntentionChangeDate(now);
              userRepo.saveAndFlush(user);
          } else {
              throw new AlovoaException("intention cooldown not finished");
          }
      }
  
      public void updateMinAge(int userMinAge) throws AlovoaException {
          if (userMinAge < minAge) {
              userMinAge = minAge;
          }
          User user = authService.getCurrentUser(true);
          user.setPreferedMinAge(userMinAge);
          userRepo.saveAndFlush(user);
      }
  
      public void updateMaxAge(int userMaxAge) throws AlovoaException {
          if (userMaxAge > maxAge) {
              userMaxAge = maxAge;
          }
          User user = authService.getCurrentUser(true);
          user.setPreferedMaxAge(userMaxAge);
          userRepo.saveAndFlush(user);
      }
  
      public void updatePreferedGender(long genderId, boolean activated) throws AlovoaException {
          User user = authService.getCurrentUser(true);
          Set<Gender> list = user.getPreferedGenders();
          if (list == null) {
              list = new HashSet<>();
          }
  
          Gender g = genderRepo.findById(genderId).orElse(null);
          if (activated) {
              list.add(g);
          } else {
              list.remove(g);
          }
          user.setPreferedGenders(list);
          userRepo.saveAndFlush(user);
      }
  
      public Set<UserMiscInfo> updateUserMiscInfo(long infoValue, boolean activated) throws AlovoaException {
          User user = authService.getCurrentUser(true);
  
          Set<UserMiscInfo> list = user.getMiscInfos();
          if (list == null) {
              list = new HashSet<>();
          }
  
          if (kidsMiscInfoSet.contains(infoValue)) {
              list.removeIf(conditionToRemoveIfExistent(kidsMiscInfoSet, infoValue));
          } else if (relationshipMiscInfoSet.contains(infoValue)) {
              list.removeIf(conditionToRemoveIfExistent(relationshipMiscInfoSet, infoValue));
          } else if (drugsAlcoholMiscInfoSet.contains(infoValue)) {
              list.removeIf(conditionToRemoveIfExistent(drugsAlcoholMiscInfoSet, infoValue));
          } else if (drugsTobaccoMiscInfoSet.contains(infoValue)) {
              list.removeIf(conditionToRemoveIfExistent(drugsAlcoholMiscInfoSet, infoValue));
          } else if (drugsCannabisMiscInfoSet.contains(infoValue)) {
              list.removeIf(conditionToRemoveIfExistent(drugsCannabisMiscInfoSet, infoValue));
          } else if (drugsOtherMiscInfoSet.contains(infoValue)) {
              list.removeIf(conditionToRemoveIfExistent(drugsOtherMiscInfoSet, infoValue));
          } else if (politicsMiscInfoSet.contains(infoValue)) {
              list.removeIf(conditionToRemoveIfExistent(politicsMiscInfoSet, infoValue));
          } else if (genderIdentityMiscInfoSet.contains(infoValue)) {
              list.removeIf(conditionToRemoveIfExistent(genderIdentityMiscInfoSet, infoValue));
          } else if (religionMiscInfoSet.contains(infoValue)) {
              list.removeIf(conditionToRemoveIfExistent(religionMiscInfoSet, infoValue));
          } else if (familyMiscInfoSet.contains(infoValue)) {
              list.removeIf(conditionToRemoveIfExistent(familyMiscInfoSet, infoValue));
          } else if (relationshipTypeMiscInfoSet.contains(infoValue)) {
              list.removeIf(conditionToRemoveIfExistent(relationshipTypeMiscInfoSet, infoValue));
          }
  
          if (activated) {
              UserMiscInfo info = userMiscInfoRepo.findByValue(infoValue);
              list.add(info);
          }
          user.setMiscInfos(list);
          userRepo.saveAndFlush(user);
          return list;
      }
  
      private Predicate<UserMiscInfo> conditionToRemoveIfExistent(final Set<Long> options, final long infoValue) {
          return c -> options.contains(c.getValue()) && c.getValue() != infoValue;
      }
  
      public void addInterest(String value) throws AlovoaException {
          User user = authService.getCurrentUser(true);
  
          if (value.length() < interestMinCharSize || value.length() > interestMaxCharSize
                  || user.getInterests().size() >= interestSize) {
              throw new AlovoaException("interest_invalid_size");
          }
  
          Pattern pattern = Pattern.compile("[a-zA-Z0-9-]+");
          Matcher matcher = pattern.matcher(value);
          if (!matcher.matches()) {
              throw new AlovoaException("interest_unsupported_characters");
          }
  
          UserInterest interest = new UserInterest();
          interest.setText(value.toLowerCase());
          interest.setUser(user);
  
          if (user.getInterests() == null) {
              user.setInterests(new ArrayList<>());
          }
  
          if (user.getInterests().contains(interest)) {
              throw new AlovoaException("interest_already_exists");
          }
  
          user.getInterests().add(interest);
  
          userRepo.saveAndFlush(user);
      }
  
      public void deleteInterest(String interest) throws AlovoaException {
          User user = authService.getCurrentUser(true);
  
          Optional<UserInterest> interestOpt = user.getInterests().stream().filter(i -> i.getText().equals(interest))
                  .findFirst();
  
          if (interest.isEmpty() || interestOpt.isEmpty()) {
              throw new AlovoaException("interest_does_not_exists");
          }
  
          user.getInterests().remove(interestOpt.get());
          userRepo.saveAndFlush(user);
      }
  
      public List<UserInterestDto> getInterestAutocomplete(String name) throws AlovoaException {
          User user = authService.getCurrentUser(true);
          name = "%" + URLDecoder.decode(name, StandardCharsets.UTF_8) + "%";
          List<String> interestTexts = user.getInterests().stream().map(UserInterest::getText).collect(Collectors.toList());
          if (interestTexts.isEmpty()) {
              interestTexts.add("");
          }
          List<UserInterestDto> interests = userInterestRepo.getInterestAutocomplete(name, interestTexts,
                  PageRequest.of(0, interestAutocompleteMax));
          interests.forEach(i -> i.setCountString(Tools.largeNumberToString(i.getCount())));
          return interests;
      }
  
      public void updateShowZodiac(int showZodiac) throws AlovoaException {
          User user = authService.getCurrentUser(true);
          user.setShowZodiac(showZodiac == 1);
          userRepo.saveAndFlush(user);
      }
  
      public void updateUnits(int units) throws AlovoaException {
          User user = authService.getCurrentUser(true);
          user.setUnits(units);
          userRepo.saveAndFlush(user);
      }
  
      public List<UserImageDto> addImage(byte[] bytes, String mimeType) throws AlovoaException, IOException {
          User user = authService.getCurrentUser(true);
          if (user.getImages() != null && user.getImages().size() < imageMax) {
              UserImage img = new UserImage();
              AbstractMap.SimpleEntry<byte[], String> adjustedImage = adjustPicture(bytes, mimeType);
              img.setBin(adjustedImage.getKey());
              img.setBinMime(adjustedImage.getValue());
              img.setDate(new Date());
              img.setUser(user);
              user.getImages().add(img);
              user = userRepo.saveAndFlush(user);
              return UserImageDto.buildFromUserImages(user, this);
          } else {
              throw new AlovoaException("max_image_amount_exceeded");
          }
      }
  
      public void deleteImage(long id) throws AlovoaException {
          User user = authService.getCurrentUser(true);
          UserImage img = userImageRepo.findById(id).orElse(null);
          if (img != null && user.getImages().contains(img)) {
              user.getImages().remove(img);
              userRepo.saveAndFlush(user);
          }
      }
  
      private AbstractMap.SimpleEntry<byte[], String> adjustPicture(byte[] bytes, String mimeType) throws IOException {
          ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
          BufferedImage image = ImageIO.read(bis);
  
          if (image.getWidth() == imageLength && image.getHeight() == imageLength) {
              return new AbstractMap.SimpleEntry<>(bytes, mimeType);
          }
  
          if (image.getWidth() != image.getHeight()) {
  
              int idealLength = image.getHeight();
              boolean heightShorter = true;
              if (image.getWidth() < image.getHeight()) {
                  heightShorter = false;
                  idealLength = image.getWidth();
              }
  
              // cut to a square
              int x = 0;
              int y = 0;
  
              if (heightShorter) {
                  x = image.getWidth() / 2 - image.getHeight() / 2;
              } else {
                  y = image.getHeight() / 2 - image.getWidth() / 2;
              }
              image = image.getSubimage(x, y, idealLength, idealLength);
          }
  
          // all images are equal in size
          BufferedImage scaledImage = new BufferedImage(imageLength, imageLength, image.getType());
          Graphics2D graphics2D = scaledImage.createGraphics();
  
          graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
  
          graphics2D.drawImage(image, 0, 0, imageLength, imageLength, null);
          graphics2D.dispose();
          image = scaledImage;
  
          // image to b64
          ByteArrayOutputStream bos = new ByteArrayOutputStream();
          final String fileType = "webp";
          ImageIO.write(image, fileType, bos);
          return new AbstractMap.SimpleEntry<>(bos.toByteArray(), MediaService.MEDIA_TYPE_IMAGE_WEBP);
      }
  
      public void likeUser(UUID uuid, String message) throws AlovoaException {
          User user = findUserByUuid(uuid);
          User currUser = authService.getCurrentUser(true);
  
          if (user.equals(currUser)) {
              throw new AlovoaException("user_is_same_user");
          }
  
          if (user.getBlockedUsers().stream().filter(o -> o.getUserTo().getId() != null)
                  .anyMatch(o -> o.getUserTo().getId().equals(currUser.getId()))) {
              throw new AlovoaException("is_blocked");
          }
  
          if (currUser.getBlockedUsers().stream().filter(o -> o.getUserTo().getId() != null)
                  .anyMatch(o -> o.getUserTo().getId().equals(user.getId()))) {
              throw new AlovoaException("is_blocked");
          }
  
          if (!Tools.usersCompatible(currUser, user, ignoreIntention)) {
              throw new AlovoaException(ExceptionHandler.USER_NOT_COMPATIBLE);
          }
  
          if (message != null && message.length() > likeMessageLength) {
              throw new AlovoaException("max_length_exceeded");
          }
  
          if (userLikeRepo.findByUserFromAndUserTo(currUser, user) == null) {
              UserLike like = new UserLike();
              like.setDate(new Date());
              like.setUserFrom(currUser);
              like.setUserTo(user);
              currUser.getLikes().add(like);
  
              if (!currUser.isDisabled()) {
                  UserNotification not = new UserNotification();
                  not.setContent(UserNotification.USER_LIKE);
                  not.setDate(new Date());
                  not.setUserFrom(currUser);
                  not.setUserTo(user);
                  not.setMessage(message);
                  currUser.getNotifications().add(not);
                  user.getDates().setNotificationDate(new Date());
                  currUser.getHiddenUsers().removeIf(hide -> hide.getUserTo().getId().equals(user.getId()));
              }
  
              userRepo.saveAndFlush(user);
              userRepo.saveAndFlush(currUser);
  
              final boolean isMatch = user.getLikes().stream().filter(o -> o.getUserTo().getId() != null)
                      .anyMatch(o -> o.getUserTo().getId().equals(currUser.getId()));
  
              if (!currUser.isDisabled() && isMatch) {
                  Conversation convo = new Conversation();
                  convo.setUsers(new ArrayList<>());
                  convo.setDate(new Date());
                  convo.getUsers().add(currUser);
                  convo.getUsers().add(user);
                  convo.setLastUpdated(new Date());
                  convo.setMessages(new ArrayList<>());
                  conversationRepo.saveAndFlush(convo);
  
                  user.getConversations().add(convo);
                  currUser.getConversations().add(convo);
  
                  userRepo.saveAndFlush(currUser);
                  userRepo.saveAndFlush(user);
  
                  if (user.getUserSettings().isEmailLike()) {
                      mailService.sendMatchNotificationMail(user);
                  }
  
              } else if (!currUser.isDisabled() && user.getUserSettings().isEmailLike()) {
                  mailService.sendLikeNotificationMail(user);
              }
          }
      }
  
      public void hideUser(UUID uuid)
              throws NumberFormatException, AlovoaException {
          User user = findUserByUuid(uuid);
          User currUser = authService.getCurrentUser(true);
          if (userHideRepo.findByUserFromAndUserTo(currUser, user) == null) {
              UserHide hide = new UserHide();
              hide.setDate(new Date());
              hide.setUserFrom(currUser);
              hide.setUserTo(user);
              currUser.getHiddenUsers().add(hide);
              userRepo.saveAndFlush(currUser);
  
              if (user.getLikes().stream().anyMatch(l -> l.getUserTo().getId().equals(currUser.getId()))) {
                  blockUser(uuid);
              }
          }
      }
  
      public void blockUser(UUID uuid)
              throws AlovoaException, NumberFormatException {
          User user = findUserByUuid(uuid);
          User currUser = authService.getCurrentUser(true);
          if (userBlockRepo.findByUserFromAndUserTo(currUser, user) == null) {
              UserBlock block = new UserBlock();
              block.setDate(new Date());
              block.setUserFrom(currUser);
              block.setUserTo(user);
              currUser.getBlockedUsers().add(block);
              userRepo.saveAndFlush(currUser);
          }
      }
  
      public void unblockUser(UUID uuid)
              throws AlovoaException, NumberFormatException {
          User user = findUserByUuid(uuid);
          User currUser = authService.getCurrentUser(true);
  
          UserBlock block = userBlockRepo.findByUserFromAndUserTo(currUser, user);
          if (block != null) {
              currUser.getBlockedUsers().remove(block);
              userRepo.save(currUser);
          }
      }
  
      public UserReport reportUser(UUID uuid, String comment)
              throws AlovoaException, NumberFormatException {
          User user = findUserByUuid(uuid);
          User currUser = authService.getCurrentUser(true);
          if (userReportRepo.findByUserFromAndUserTo(currUser, user) == null) {
              UserReport report = new UserReport();
              report.setDate(new Date());
              report.setUserFrom(currUser);
              report.setUserTo(user);
              report.setComment(comment);
              currUser.getReported().add(report);
              currUser = userRepo.saveAndFlush(currUser);
  
              return currUser.getReported().get(currUser.getReported().size() - 1);
          }
  
          return null;
      }
  
      public boolean hasNewAlert() throws AlovoaException {
          return hasNewAlert(null);
      }
  
      public boolean hasNewAlert(String lang) throws AlovoaException {
          User user = authService.getCurrentUser(true);
          // user always check their alerts periodically in the background, so just update
          // it here
          if (user != null) {
              updateUserInfo(user, lang);
              return user.getDates().getNotificationDate().after(user.getDates().getNotificationCheckedDate());
          } else {
              return false;
          }
      }
  
      public boolean hasNewMessage() throws AlovoaException {
          User user = authService.getCurrentUser(true);
          if (user != null && user.getDates().getMessageDate() != null
                  && user.getDates().getMessageCheckedDate() != null) {
              return user.getDates().getMessageDate().after(user.getDates().getMessageCheckedDate());
          } else {
              return false;
          }
      }
  
      public void updateUserInfo(User user) {
          updateUserInfo(user, null);
      }
  
      public void updateUserInfo(User user, String language) {
          if (user != null && user.getDates() != null) {
              user.getDates().setActiveDate(new Date());
              String lang;
              if (language == null) {
                  lang = LocaleContextHolder.getLocale().getLanguage();
              } else {
                  lang = language;
              }
              if (!lang.equals(user.getLanguage())) {
                  user.setLanguage(lang);
              }
              userRepo.saveAndFlush(user);
          }
      }
  
      public ResponseEntity<Resource> getUserdata(UUID uuid) throws AlovoaException, JsonProcessingException,
              NumberFormatException {
          User user = authService.getCurrentUser(true);
          User userFromUuid = findUserByUuid(uuid);
          if (!user.getId().equals(userFromUuid.getId())) {
              throw new AlovoaException("users_not_equal");
          }
  
          UserGdpr ug = UserGdpr.userToUserGdpr(user);
          String json = objectMapper.writeValueAsString(ug);
          ByteArrayResource resource = new ByteArrayResource(json.getBytes(StandardCharsets.UTF_8));
  
          ContentDisposition contentDisposition = ContentDisposition.builder("inline").filename("alovoa_userdata.json")
                  .build();
  
          MediaType mediaType = MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
          HttpHeaders headers = new HttpHeaders();
          headers.setContentType(mediaType);
          headers.setContentDisposition(contentDisposition);
  
          return new ResponseEntity<>(resource, headers, HttpStatus.OK);
      }
  
      @SuppressWarnings("deprecation")
      public String getAudio(UUID uuid)
              throws NumberFormatException, AlovoaException {
          User user = findUserByUuid(uuid);
          if (user.getAudio() == null) {
              return null;
          }
          return user.getAudio().getData();
      }
  
      public void deleteAudio() throws AlovoaException {
          User user = authService.getCurrentUser(true);
          user.setAudio(null);
          userRepo.saveAndFlush(user);
      }
  
      @SuppressWarnings("deprecation")
      public void updateAudio(byte[] bytes, String mimeType)
              throws AlovoaException, UnsupportedAudioFileException, IOException {
          User user = authService.getCurrentUser(true);
          byte[] newAudioB64 = adjustAudio(bytes, mimeType);
          if (user.getAudio() == null) {
              UserAudio audio = new UserAudio();
              audio.setBin(newAudioB64);
              audio.setUser(user);
              audio.setUuid(UUID.randomUUID());
              user.setAudio(audio);
          } else {
              user.getAudio().setBin(newAudioB64);
              user.getAudio().setData(null);
              user.getAudio().setUuid(UUID.randomUUID());
          }
          userRepo.saveAndFlush(user);
      }
  
      public void updateCountry(String countryIso) throws AlovoaException {
          User user = authService.getCurrentUser(true);
          boolean validCountryIso = Arrays.asList(Locale.getISOCountries()).contains(countryIso);
          if (validCountryIso) {
              user.setCountry(countryIso);
          } else {
              // remove flag if user is not in a valid country
              user.setCountry(null);
          }
  
          userRepo.saveAndFlush(user);
      }
  
      public String getVerificationCode() throws AlovoaException {
          User user = authService.getCurrentUser(true);
          return getVerificationCode(user);
      }
  
      public String getVerificationCode(User user) {
          if (user.getVerificationCode() != null) {
              return user.getVerificationCode();
          } else {
              String verificationString = new String(OxCaptcha.genText(verificationStringLength));
              user.setVerificationCode(verificationString);
              userRepo.saveAndFlush(user);
              return verificationString;
          }
      }
  
      public void updateVerificationPicture(byte[] bytes, String mimeType) throws AlovoaException, IOException {
          User user = authService.getCurrentUser(true);
  
          //verification picture only usable with profile picture
          if (user.getProfilePicture() == null) {
              throw new AlovoaException("need_profile_picture");
          }
          user.setVerificationPicture(null);
          userRepo.saveAndFlush(user);
          AbstractMap.SimpleEntry<byte[], String> adjustedImage = adjustPicture(bytes, mimeType);
          UserVerificationPicture verificationPicture = new UserVerificationPicture();
          verificationPicture.setBin(adjustedImage.getKey());
          verificationPicture.setBinMime(adjustedImage.getValue());
          verificationPicture.setDate(new Date());
          verificationPicture.setUser(user);
          verificationPicture.setUserNo(new ArrayList<>());
          verificationPicture.setUserYes(new ArrayList<>());
          verificationPicture.setUuid(UUID.randomUUID());
          user.setVerificationPicture(verificationPicture);
          userRepo.saveAndFlush(user);
      }
  
      public void upvoteVerificationPicture(UUID uuid) throws AlovoaException, IOException, InvalidAlgorithmParameterException, IllegalBlockSizeException, NoSuchPaddingException, BadPaddingException, NoSuchAlgorithmException, InvalidKeyException {
          User currentUser = authService.getCurrentUser(true);
          User user = findUserByUuid(uuid);
  
          if (currentUser.equals(user)) {
              throw new AlovoaException("invalid_user");
          }
  
          if (user.getVerificationPicture() == null) {
              throw new AlovoaException("no_verification_picture");
          }
  
          if (user.getVerificationPicture().getUserYes().contains(currentUser) || user.getVerificationPicture().getUserNo().contains(currentUser)) {
              throw new AlovoaException("already_voted");
          }
  
          user.getVerificationPicture().getUserYes().add(currentUser);
          userRepo.saveAndFlush(user);
      }
  
      public void downvoteVerificationPicture(UUID uuid) throws AlovoaException {
          User currentUser = authService.getCurrentUser(true);
          User user = findUserByUuid(uuid);
  
          if (currentUser.equals(user)) {
              throw new AlovoaException("invalid_user");
          }
  
          if (user.getVerificationPicture() == null) {
              throw new AlovoaException("no_verification_picture");
          }
  
          if (user.getVerificationPicture().getUserYes().contains(currentUser) || user.getVerificationPicture().getUserNo().contains(currentUser)) {
              throw new AlovoaException("already_voted");
          }
  
          user.getVerificationPicture().getUserNo().add(currentUser);
          userRepo.saveAndFlush(user);
      }
  
      public void updateUserLocation(Double latitude, Double longitude) throws AlovoaException {
          User user = authService.getCurrentUser(true);
          // rounding to improve privacy
          DecimalFormat df = new DecimalFormat("#.##");
          df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH));
          user.setLocationLatitude(Double.valueOf(df.format(latitude)));
          user.setLocationLongitude(Double.valueOf(df.format(longitude)));
          userRepo.saveAndFlush(user);
      }
  
      public void updateProfilePictureUUID(UserProfilePicture image, UUID uuid) {
          if (image.getUuid() == null) {
              image.setUuid(uuid);
              userProfilePictureRepository.saveAndFlush(image);
          }
      }
  
      public void updateUUID(User user, UUID uuid) {
          if (user.getUuid() == null) {
              user.setUuid(uuid);
              userRepo.saveAndFlush(user);
          }
      }
  
      public void updateImageUUID(UserImage image, UUID uuid) {
          if (image.getUuid() == null) {
              image.setUuid(uuid);
              userImageRepo.saveAndFlush(image);
          }
      }
  
      public User findUserByUuid(UUID uuid) throws AlovoaException {
          User user = userRepo.findByUuid(uuid);
          if (user == null) {
              throw new AlovoaException("user_not_found: " + uuid);
          }
          return user;
      }
  
      private byte[] adjustAudio(byte[] bytes, String mimeType) throws UnsupportedAudioFileException, IOException {
          if (mimeType.equals(MIME_X_WAV) || mimeType.equals(MIME_WAV)) {
              return trimAudioWav(bytes, audioMaxTime);
          } else if (mimeType.equals(MIME_MPEG) || mimeType.equals(MIME_MP3)) {
              return trimAudioWav(convertAudioMp3Wav(bytes), audioMaxTime);
          }
          return null;
      }
  
      private byte[] trimAudioWav(byte[] bytes, int maxSeconds) throws UnsupportedAudioFileException, IOException {
  
          ByteArrayInputStream bis;
          ByteArrayOutputStream baos;
          AudioInputStream ais = null;
          AudioInputStream aisShort = null;
  
          try {
              bis = new ByteArrayInputStream(bytes);
              ais = AudioSystem.getAudioInputStream(bis);
              AudioFormat format = ais.getFormat();
              long frameLength = ais.getFrameLength();
  
              // check if audio is shorter or equal max length
              double durationInSeconds = frameLength * format.getFrameRate();
              if (durationInSeconds < 0.0) {
                  durationInSeconds = durationInSeconds * (-1);
              }
              if (durationInSeconds <= maxSeconds) {
                  ais.close();
                  return bytes;
              } else {
                  long frames = (long) (format.getFrameRate() * maxSeconds);
                  aisShort = new AudioInputStream(ais, format, frames);
                  AudioFileFormat.Type targetType = AudioFileFormat.Type.WAVE;
                  baos = new ByteArrayOutputStream();
                  AudioSystem.write(aisShort, targetType, baos);
                  aisShort.close();
                  ais.close();
                  return baos.toByteArray();
              }
          } catch (Exception e) {
              if (ais != null) {
                  ais.close();
              }
              if (aisShort != null) {
                  aisShort.close();
              }
              throw e;
          }
      }
  }
  