content: package com.nonononoki.alovoa.service;
  
  import com.nonononoki.alovoa.Tools;
  import com.nonononoki.alovoa.component.TextEncryptorConverter;
  import com.nonononoki.alovoa.entity.User;
  import com.nonononoki.alovoa.entity.user.Gender;
  import com.nonononoki.alovoa.entity.user.UserIntention;
  import com.nonononoki.alovoa.model.AlovoaException;
  import com.nonononoki.alovoa.model.SearchDto;
  import com.nonononoki.alovoa.model.SearchDto.SearchStage;
  import com.nonononoki.alovoa.model.UserDto;
  import com.nonononoki.alovoa.model.UserSearchRequest;
  import com.nonononoki.alovoa.repo.GenderRepository;
  import com.nonononoki.alovoa.repo.UserRepository;
  import lombok.Builder;
  import lombok.Getter;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.beans.factory.annotation.Value;
  import org.springframework.data.domain.PageRequest;
  import org.springframework.data.domain.Sort;
  import org.springframework.stereotype.Component;
  import org.springframework.stereotype.Service;
  import uk.recurse.geocoding.reverse.Country;
  import uk.recurse.geocoding.reverse.ReverseGeocoder;
  
  import javax.crypto.BadPaddingException;
  import javax.crypto.IllegalBlockSizeException;
  import javax.crypto.NoSuchPaddingException;
  import java.io.UnsupportedEncodingException;
  import java.security.InvalidAlgorithmParameterException;
  import java.security.InvalidKeyException;
  import java.security.NoSuchAlgorithmException;
  import java.util.*;
  import java.util.stream.Collectors;
  
  @Service
  public class SearchService {
  
      public static final int SORT_DISTANCE = 1;
      public static final int SORT_ACTIVE_DATE = 2;
      public static final int SORT_INTEREST = 3;
      public static final int SORT_DEFAULT = 4;
      public static final int SORT_DONATION_TOTAL = 5;
      public static final int SORT_NEWEST_USER = 6;
      private static final double LATITUDE = 111.1;
      private static final double LONGITUDE = 111.320;
      private static final int DEFAULT_DISTANCE = 150;
      private static final int SEARCH_MAX = 200;
      private static ReverseGeocoder geocoder = null;
  
      private static final Set<Long> ALL_INTENTIONS = Set.of(UserIntention.MEET, UserIntention.DATE, UserIntention.SEX);
      private static final Set<Long> ALL_GENDER_IDS = Set.of(Gender.MALE, Gender.FEMALE, Gender.OTHER);
  
      @Autowired
      private TextEncryptorConverter textEncryptor;
      @Autowired
      private AuthService authService;
      @Autowired
      private UserRepository userRepo;
      @Autowired
      private PublicService publicService;
      @Autowired
      private UserService userService;
      @Autowired
      private GenderRepository genderRepo;
      @Value("${app.search.max}")
      private int maxResults;
      @Value("${app.search.max.distance}")
      private int maxDistance;
      @Value("${app.age.min}")
      private int ageMin;
      @Value("${app.age.max}")
      private int ageMax;
      @Value("${app.search.estimate.max}")
      private int searchEstimateMax;
      @Value("${app.search.ignore-intention}")
      private boolean ignoreIntention;
  
      public ReverseGeocoder getGeocoder() {
          if (geocoder == null) {
              geocoder = new ReverseGeocoder();
          }
          return geocoder;
      }
  
      @Deprecated
      public SearchDto searchDefault() throws AlovoaException, InvalidKeyException, IllegalBlockSizeException,
              BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException,
              UnsupportedEncodingException {
          User user = authService.getCurrentUser(true);
          if (user.isAdmin()) {
              return SearchDto.builder().users(searchResultsToUserDto(userRepo.adminSearch(), 0, user)).build();
          }
          if (user.getLocationLatitude() != null) {
              return searchComplete(SearchParams.builder().latitude(user.getLocationLatitude())
                      .longitude(user.getLocationLongitude()).showOutsideParameters(true).build());
          } else {
              return null;
          }
      }
  
      @Deprecated
      public SearchDto search(Double latitude, Double longitude, int distance, int sortId) throws AlovoaException,
              InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException,
              NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException {
          return searchComplete(SearchParams.builder().latitude(latitude)
                  .longitude(longitude).showOutsideParameters(true).distance(distance).sort(sortId).build());
      }
  
      public SearchDto searchComplete(SearchParams params) throws AlovoaException,
              InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException,
              NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException {
  
          User user = authService.getCurrentUser(true);
          if (user.isAdmin()) {
              return SearchDto.builder().users(searchResultsToUserDto(userRepo.adminSearch(), 0, user)).build();
          }
  
          Double latitude = params.getLatitude();
          Double longitude = params.getLongitude();
  
          int distance = params.getDistance();
          int sortId = params.getSort();
          boolean showOutsideParameters = params.isShowOutsideParameters();
          Set<Long> intentions = params.getIntentions().isEmpty() ? ALL_INTENTIONS : params.getIntentions();
          Set<String> interests = params.getInterests();
          Set<Integer> miscInfoIds = params.getMiscInfos();
  
          Set<Long> preferredGenderIds;
          Set<Long> userPreferredGenderIds =  user.getPreferedGenders().stream().map(Gender::getId).collect(Collectors.toSet());
          boolean updatedPreferredGenderIds = !params.getPreferredGenderIds().isEmpty() && !Objects.equals(userPreferredGenderIds, params.getPreferredGenderIds());
          if(updatedPreferredGenderIds)  {
              user.setPreferedGenders(new HashSet<>(genderRepo.findAllById(params.getPreferredGenderIds())));
              preferredGenderIds = params.getPreferredGenderIds();
          } else {
              preferredGenderIds = userPreferredGenderIds;
          }
  
          if (!latitude.equals(user.getLocationLatitude()) || !longitude.equals(user.getLocationLongitude()) || user.getCountry() == null) {
              Optional<String> country = getCountryIsoByLocation(latitude, longitude);
              if (country.isPresent()) {
                  userService.updateCountry(country.get());
              }
          }
  
          Sort sort = switch (sortId) {
              case SORT_DEFAULT -> Sort.by(Sort.Direction.DESC, "dates.latestDonationDate", "dates.creationDate");
              case SORT_ACTIVE_DATE -> Sort.by(Sort.Direction.DESC, "dates.activeDate");
              case SORT_DONATION_TOTAL -> Sort.by(Sort.Direction.DESC, "totalDonations");
              case SORT_NEWEST_USER -> Sort.by(Sort.Direction.DESC, "dates.creationDate");
              default -> Sort.unsorted();
          };
  
          int ageLegal = Tools.AGE_LEGAL;
  
          if (distance > maxDistance) {
              throw new AlovoaException("max_distance_exceeded");
          }
  
          user.getDates().setActiveDate(new Date());
  
          userService.updateUserLocation(latitude, longitude);
  
          int age = Tools.calcUserAge(user);
          boolean isLegalAge = age >= ageLegal;
          int minAge = params.getPreferredMinAge() == null ? user.getPreferedMinAge() : params.getPreferredMinAge();
          int maxAge = params.getPreferredMaxAge() == null ? user.getPreferedMaxAge() : params.getPreferredMaxAge();
  
          user.setPreferedMinAge(minAge);
          user.setPreferedMaxAge(maxAge);
  
          if (isLegalAge && minAge < ageLegal) {
              minAge = ageLegal;
          }
          if (!isLegalAge && maxAge >= ageLegal) {
              maxAge = ageLegal - 1;
          }
  
          Date minDate = Tools.ageToDate(maxAge);
          Date maxDate = Tools.ageToDate(minAge);
  
          double deltaLongFactor = LONGITUDE * Math.cos(latitude / 180.0 * Math.PI);
          double deltaLat = distance / LATITUDE;
          double deltaLong = distance / deltaLongFactor;
          double minLat = latitude - deltaLat;
          double maxLat = latitude + deltaLat;
          double minLong = longitude - deltaLong;
          double maxLong = longitude + deltaLong;
  
          userRepo.saveAndFlush(user);
  
          UserSearchRequest request = UserSearchRequest.builder().age(age).minLat(minLat).minLong(minLong).maxLat(maxLat)
                  .maxLong(maxLong).maxDateDob(maxDate).minDateDob(minDate).intentionIds(intentions).preferedGender(user.getGender())
                  .likeIds(user.getLikes().stream().map(o -> o.getUserTo() != null ? o.getUserTo().getId() : 0).collect(Collectors.toSet()))
                  .blockIds(user.getBlockedUsers().stream().map(o -> o.getUserTo() != null ? o.getUserTo().getId() : 0).collect(Collectors.toSet()))
                  .hideIds(user.getHiddenUsers().stream().map(o -> o.getUserTo() != null ? o.getUserTo().getId() : 0).collect(Collectors.toSet()))
                  .genderIds(preferredGenderIds)
                  .blockedByIds(user.getBlockedByUsers().stream().map(o -> o.getUserFrom() != null ? o.getUserFrom().getId() : 0).collect(Collectors.toSet()))
                  .miscInfos(miscInfoIds)
                  .interests(interests)
                  .build();
  
          request.getBlockIds().add(user.getId());
  
          List<User> users;
          if(request.getMiscInfos().isEmpty() && request.getInterests().isEmpty()) {
              users = userRepo.usersSearchNoExtras(request, PageRequest.of(0, SEARCH_MAX, sort));
          } else if(!request.getMiscInfos().isEmpty() && request.getInterests().isEmpty()) {
              users = userRepo.usersSearchMisc(request, PageRequest.of(0, SEARCH_MAX, sort));
          } else if(request.getMiscInfos().isEmpty()) {
              users = userRepo.usersSearchInterest(request, PageRequest.of(0, SEARCH_MAX, sort));
          } else {
              users = userRepo.usersSearchInterestMisc(request, PageRequest.of(0, SEARCH_MAX, sort));
          }
  
          if (!users.isEmpty()) {
              return SearchDto.builder().users(searchResultsToUserDto(users, sortId, user))
                      .stage(SearchStage.NORMAL).build();
          }
  
          // NO COMPATIBLE USERS FOUND
          if(showOutsideParameters) {
  
              //SEARCH COMPATIBLE USERS
              request.setIntentionIds(ALL_INTENTIONS);
              request.setMiscInfos(Set.of());
              users = userRepo.usersBaseSearch(request, PageRequest.of(0, SEARCH_MAX, sort));
              if (!users.isEmpty()) {
                  return SearchDto.builder().users(searchResultsToUserDto(users, sortId, user))
                          .global(false).build();
              }
  
              // NO COMPATIBLE USERS FOUND NEARBY, SEARCH AROUND THE WORLD!
              users = userRepo.usersSearchAllIgnoreLocation(request, PageRequest.of(0, SEARCH_MAX, sort));
                  return SearchDto.builder().users(searchResultsToUserDto(users, sortId, user))
                          .global(true).stage(SearchStage.WORLD).build();
          } else {
              return SearchDto.builder().users(List.of()).build();
          }
      }
  
      private List<UserDto> searchResultsToUserDto(final List<User> userList, int sort, User user)
              throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException, NoSuchAlgorithmException,
              NoSuchPaddingException, InvalidAlgorithmParameterException, UnsupportedEncodingException, AlovoaException {
          List<UserDto> userDtos = new ArrayList<>();
          for (User u : userList) {
              UserDto dto = UserDto.userToUserDto(UserDto.DtoBuilder.builder().ignoreIntention(ignoreIntention)
                      .currentUser(user).user(u).userService(userService).build());
              userDtos.add(dto);
          }
  
          if (sort == SORT_DISTANCE) {
              userDtos = userDtos.stream().sorted(Comparator.comparing(UserDto::getDistanceToUser))
                      .collect(Collectors.toList());
          } else if (sort == SORT_INTEREST) {
              Comparator<UserDto> comparatorCommonInterest = Comparator.comparing(f -> f.getCommonInterests().size());
              userDtos = userDtos.stream().filter(f -> !f.getCommonInterests().isEmpty())
                      .sorted(comparatorCommonInterest.reversed()
                              .thenComparing(Comparator.comparing(UserDto::getDistanceToUser).reversed()))
                      .collect(Collectors.toList());
          }
  
          return userDtos;
      }
  
      public Optional<String> getCountryIsoByLocation(double lat, double lon) {
          Optional<Country> country = getGeocoder().getCountry(lat, lon);
          return country.map(Country::iso);
      }
  
      @Getter
      @Builder
      public static class SearchParams {
          @Builder.Default
          private Set<Long> preferredGenderIds = new HashSet<>();
          @Builder.Default
          private Integer preferredMinAge = null;
          @Builder.Default
          private Integer preferredMaxAge = null;
          @Builder.Default
          private int distance = DEFAULT_DISTANCE;
          private boolean showOutsideParameters;
          @Builder.Default
          private int sort = SORT_DEFAULT;
          private double latitude;
          private double longitude;
          @Builder.Default
          private Set<Integer> miscInfos = new HashSet<>();
          @Builder.Default
          private Set<Long> intentions = new HashSet<>();
          @Builder.Default
          private Set<String> interests = new HashSet<>();
      }
  
  }
  