UserService.java

package com.paymybuddy.paymybuddy.service;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.paymybuddy.paymybuddy.dto.BalanceOperationDTO;
import com.paymybuddy.paymybuddy.dto.BuddiesDTO;
import com.paymybuddy.paymybuddy.dto.BuddyConnectionDTO;
import com.paymybuddy.paymybuddy.dto.BuddyForTransferDTO;
import com.paymybuddy.paymybuddy.dto.RegisterUserDTO;
import com.paymybuddy.paymybuddy.dto.TransactionInListDTO;
import com.paymybuddy.paymybuddy.dto.UpdateUserDTO;
import com.paymybuddy.paymybuddy.dto.UserDTO;
import com.paymybuddy.paymybuddy.exception.UsernameAlreadyTakenException;
import com.paymybuddy.paymybuddy.exception.BuddyAlreadyAddedException;
import com.paymybuddy.paymybuddy.exception.BuddyNotFoundException;
import com.paymybuddy.paymybuddy.exception.EmailAlreadyUsedException;
import com.paymybuddy.paymybuddy.exception.NotFoundException;
import com.paymybuddy.paymybuddy.exception.SelfAddException;
import com.paymybuddy.paymybuddy.model.Transaction;
import com.paymybuddy.paymybuddy.model.User;
import com.paymybuddy.paymybuddy.repository.TransactionRepository;
import com.paymybuddy.paymybuddy.repository.UserRepository;

import jakarta.persistence.EntityNotFoundException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Service
@AllArgsConstructor
@Slf4j
public class UserService {
    
    private final UserRepository userRepository;
    private final TransactionRepository transactionRepository;

    private final PasswordEncoder passwordEncoder;

    private final BalanceService operationService;

    @Autowired
    private AuthenticationManager authenticationManager;

    public boolean existsByUsername(String username){
        return userRepository.existsByUsername(username.toLowerCase());
    }

    public boolean existsByEmail(String email){
        return userRepository.existsByEmail(email.trim().toLowerCase());
    }

    @Transactional
    public User createUser(RegisterUserDTO userDTO){
        log.debug("*** Creating user: {}", userDTO);
        
        String lowerCaseUserName = userDTO.getUsername().toLowerCase();
        if(userRepository.existsByUsername(lowerCaseUserName)){
            log.error("*** Username already taken: {}", lowerCaseUserName);
            throw new UsernameAlreadyTakenException("Username already taken: " + lowerCaseUserName);
        }
        
        String normalizedEmail = userDTO.getEmail().trim().toLowerCase();
        if(userRepository.existsByEmail(normalizedEmail)){
            log.error("*** Email already used: {}", normalizedEmail);
            throw new EmailAlreadyUsedException("Email already used: " + normalizedEmail);
        }
        
        User user = new User();
        user.setUsername(lowerCaseUserName);
        user.setEmail(normalizedEmail);
        user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
        user.setBalance(BigDecimal.ZERO);
        user.setBuddies(new HashSet<>());
        user.setDateCreated(LocalDateTime.now());

        User createdUser = userRepository.save(user);
        log.info("*** User created: {}, {}", createdUser.getEmail(), createdUser.getUsername());
        
        return createdUser;
    }

    @Transactional
    public User updateUser(UpdateUserDTO userDTO){
        log.debug("*** Updating user: {}", userDTO);

        User user = userRepository.getById(userDTO.getId());

        if(!userDTO.getUsername().equals(user.getUsername())){
            String lowerCaseUserName = userDTO.getUsername().toLowerCase();
            if(userRepository.existsByUsername(lowerCaseUserName)){
                log.error("*** Username already taken: {}", lowerCaseUserName);
                throw new UsernameAlreadyTakenException("Username already taken: " + lowerCaseUserName);
            } else user.setUsername(lowerCaseUserName);
        }
        
        if(!userDTO.getEmail().equals(user.getEmail())){
            String normalizedEmail = userDTO.getEmail().trim().toLowerCase();
            if(userRepository.existsByEmail(normalizedEmail)){
                log.error("*** Email already used: {}", normalizedEmail);
                throw new EmailAlreadyUsedException("Email already used: " + normalizedEmail);
            } else user.setEmail(normalizedEmail);
        }
        
        if(!userDTO.getPassword().isEmpty()){
            user.setPassword(passwordEncoder.encode(userDTO.getPassword()));
        }
        
        User updatedUser = userRepository.save(user);
        log.info("*** User updated: {}, {}", updatedUser.getEmail(), updatedUser.getUsername());
        
        return updatedUser;
    }

    public UserDTO getUserById(int id){
        log.debug("*** Getting user by id: {}", id);
        User user = userRepository.findById(id).orElse(null);
        if (user == null){
            throw new NotFoundException("User not found with id " + id);
        }
        return new UserDTO(user.getUsername(), user.getEmail(), user.getBuddies());
    }

    //on considère qu'il n'y a pas de réciprocité ni d'acceptation d'ajout
    @Transactional
    public void addBuddy(BuddyConnectionDTO buddyConnection){
        log.debug("*** Adding buddy: {}", buddyConnection);

        if(buddyConnection.userEmail().equals(buddyConnection.buddyEmail())){
            throw new SelfAddException(buddyConnection.userEmail() + " tried to add himself in his list");
        };

        User user = userRepository.findByEmail(buddyConnection.userEmail())
                .orElseThrow(()-> new NotFoundException("User not found with email " + buddyConnection.userEmail()));
                
        User buddy = userRepository.findByEmail(buddyConnection.buddyEmail())
                .orElseThrow(()-> new BuddyNotFoundException("Buddy not found with email " + buddyConnection.buddyEmail(), buddyConnection.buddyEmail()));

        if (user.getBuddies().contains(buddy)){
            throw new BuddyAlreadyAddedException("Buddy connection between " + 
                buddyConnection.userEmail() + " and " + buddyConnection.buddyEmail() + "already exists", buddy.getUsername());
        };
        
        user.getBuddies().add(buddy);
        userRepository.save(user);

        log.info("*** Buddy {} added to {} 's list", buddyConnection.buddyEmail(), buddyConnection.userEmail());
    }
  

    @Transactional
    public void removeBuddy(BuddyConnectionDTO buddyConnection){
        log.debug("*** Removing buddy: {}", buddyConnection);

        User user = userRepository.findByEmail(buddyConnection.userEmail())
                .orElseThrow(()-> new NotFoundException("User not found with email " + buddyConnection.userEmail()));
                
        User buddy = userRepository.findByEmail(buddyConnection.buddyEmail())
                .orElseThrow(()-> new NotFoundException("Buddy not found with email " + buddyConnection.buddyEmail()));
        
        if (!user.getBuddies().contains(buddy)){
            throw new NotFoundException("Buddy connection between " + 
                buddyConnection.userEmail() + " and " + buddyConnection.buddyEmail() + " does not exist");
        }
        
        user.getBuddies().remove(buddy);
        userRepository.save(user);

        log.info("*** Buddy {} removed from {} 's list", buddyConnection.buddyEmail(), buddyConnection.userEmail());
    }
    
    @Transactional
    public void deposit(BalanceOperationDTO operation){
        log.debug("*** Processing deposit operation: {}", operation);
        operationService.updateBalance(operation, true);
    }

    public Page<TransactionInListDTO> getTransactionsPaginated(User user, int page, int size){
        return getTransactionsPaginated(user.getId(), page, size);
    }
    
    public Page<TransactionInListDTO> getTransactionsPaginated(int userId, int page, int size){
        log.debug("*** Getting transactions for userId: {}", userId);

        Pageable pageable = PageRequest.of(page, size, Sort.by("dateCreated").descending());

        Page<Transaction> transactionsPage =
            transactionRepository.findBySenderIdOrReceiverIdOrderByDateCreatedDesc(userId, userId, pageable);

        return transactionsPage.map(transaction -> new TransactionInListDTO(
            transaction.getDateCreated(),
            transaction.getSender().getUsername(),
            transaction.getReceiver().getUsername(),
            transaction.getAmount(),
            transaction.getDescription()
        ));
    }

    public User getUserByEmail(String email) {
        log.debug("*** Getting user by email: {}", email);

        return userRepository.findByEmail(email).orElseThrow(() -> new UsernameNotFoundException("Utilisateur non trouvé: " + email));
    }

    public void authenticateUser(HttpServletRequest request, String email, String password){

        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(email, password);
        Authentication authentication = authenticationManager.authenticate(token);
        SecurityContextHolder.getContext().setAuthentication(authentication);

        log.info("* User connected: {}", email);
    }

    public BuddiesDTO getBuddies(int userId) {
        log.debug("*** Getting buddies for userId: {}", userId);

        User user = userRepository.findById(userId)
            .orElseThrow(() -> new EntityNotFoundException("Utilisateur non trouvé"));

        return new BuddiesDTO(user.getBuddies()
                            .stream()
                            .map(buddy -> new BuddyForTransferDTO(
                                buddy.getId(),
                                buddy.getUsername(),
                                buddy.getEmail()
                            ))
                            .collect(Collectors.toSet()));
    }
}