AuthenticationService.java

package org.petify.backend.services;

import org.petify.backend.dto.LoginRequestDTO;
import org.petify.backend.dto.LoginResponseDTO;
import org.petify.backend.dto.RegistrationDTO;
import org.petify.backend.models.ApplicationUser;
import org.petify.backend.models.Role;
import org.petify.backend.models.VolunteerStatus;
import org.petify.backend.repository.RoleRepository;
import org.petify.backend.repository.UserRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;

@Service
@Transactional
public class AuthenticationService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private RoleRepository roleRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private TokenService tokenService;

    @Autowired
    private AchievementService achievementService;

    public ApplicationUser registerUser(final RegistrationDTO registrationDTO) {
        final String username = (registrationDTO.getUsername() != null && !registrationDTO.getUsername().isEmpty())
                ? registrationDTO.getUsername()
                : (registrationDTO.getEmail() != null)
                ? registrationDTO.getEmail()
                : registrationDTO.getPhoneNumber();

        if (userRepository.findByEmailOrPhoneNumber(
                registrationDTO.getEmail(),
                registrationDTO.getPhoneNumber()).isPresent()) {
            throw new IllegalArgumentException("User with this email or phone number already exists");
        }

        final String encodedPassword = passwordEncoder.encode(registrationDTO.getPassword());

        Role userRole = roleRepository.findByAuthority("USER")
                .orElseThrow(() -> new RuntimeException("Default user role not found"));

        Set<Role> authorities = new HashSet<>();
        authorities.add(userRole);

        // If user wants to create a shelter, assign SHELTER role
        if (registrationDTO.isCreateShelter()) {
            Role shelterRole = roleRepository.findByAuthority("SHELTER")
                    .orElseThrow(() -> new RuntimeException("SHELTER role not found"));
            authorities.add(shelterRole);
        }

        ApplicationUser newUser = new ApplicationUser();
        newUser.setUsername(username);
        newUser.setPassword(encodedPassword);
        newUser.setFirstName(registrationDTO.getFirstName());
        newUser.setLastName(registrationDTO.getLastName());
        newUser.setBirthDate(registrationDTO.getBirthDate());
        newUser.setGender(registrationDTO.getGender());
        newUser.setPhoneNumber(registrationDTO.getPhoneNumber());
        newUser.setEmail(registrationDTO.getEmail());
        newUser.setActive(true);
        newUser.setCreatedAt(LocalDateTime.now());

        newUser.setXpPoints(0);
        newUser.setLevel(1);
        newUser.setLikesCount(0);
        newUser.setSupportCount(0);
        newUser.setBadgesCount(0);

        newUser.setPreferredSearchDistanceKm(20.0);
        newUser.setAutoLocationEnabled(false);

        if (registrationDTO.isApplyAsVolunteer() && !registrationDTO.isCreateShelter()) { // Volunteer only if not creating shelter
            newUser.setVolunteerStatus(VolunteerStatus.PENDING);
        } else {
            newUser.setVolunteerStatus(VolunteerStatus.NONE);
        }

        newUser.setAuthorities(authorities);

        ApplicationUser savedUser = userRepository.save(newUser);

        achievementService.initializeUserAchievements(savedUser);

        return savedUser;
    }

    public LoginResponseDTO loginUser(final LoginRequestDTO loginRequest) {
        try {
            ApplicationUser user = userRepository.findByEmailOrPhoneNumber(
                            loginRequest.getLoginIdentifier(),
                            loginRequest.getLoginIdentifier())
                    .orElseThrow(() -> new RuntimeException("User not found"));

            if (!user.isActive()) {
                String deactivationReason = user.getDeactivationReason() != null
                        ? user.getDeactivationReason() : "Account has been deactivated";
                throw new DisabledException(deactivationReason);
            }

            Authentication auth = authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(user.getUsername(), loginRequest.getPassword())
            );

            String token = tokenService.generateJwt(auth);

            return new LoginResponseDTO(user, token);

        } catch (DisabledException e) {
            return new LoginResponseDTO(null, "", e.getMessage());
        } catch (AuthenticationException e) {
            return new LoginResponseDTO(null, "", "Invalid credentials");
        }
    }

    public ApplicationUser updateUserProfile(String username, ApplicationUser updatedUser) {
        ApplicationUser user = userRepository.findByUsername(username)
                .orElseThrow(() -> new RuntimeException("User not found"));

        if (updatedUser.getEmail() != null && !updatedUser.getEmail().equals(user.getEmail())) {
            if (userRepository.findByEmail(updatedUser.getEmail()).isPresent()) {
                throw new IllegalArgumentException("Email is already in use");
            }
            user.setEmail(updatedUser.getEmail());
        }

        if (updatedUser.getPhoneNumber() != null && !updatedUser.getPhoneNumber().equals(user.getPhoneNumber())) {
            if (userRepository.findByPhoneNumber(updatedUser.getPhoneNumber()).isPresent()) {
                throw new IllegalArgumentException("Phone number is already in use");
            }
            user.setPhoneNumber(updatedUser.getPhoneNumber());
        }

        if (updatedUser.getFirstName() != null) {
            user.setFirstName(updatedUser.getFirstName());
        }

        if (updatedUser.getLastName() != null) {
            user.setLastName(updatedUser.getLastName());
        }

        if (updatedUser.getBirthDate() != null) {
            user.setBirthDate(updatedUser.getBirthDate());
        }

        if (updatedUser.getGender() != null) {
            user.setGender(updatedUser.getGender());
        }

        if (updatedUser.getPreferredSearchDistanceKm() != null) {
            user.setPreferredSearchDistanceKm(updatedUser.getPreferredSearchDistanceKm());
        }

        if (updatedUser.getAutoLocationEnabled() != null) {
            user.setAutoLocationEnabled(updatedUser.getAutoLocationEnabled());
        }

        return userRepository.save(user);
    }

    public void deleteUserAccount(String username) {
        ApplicationUser user = userRepository.findByUsername(username)
                .orElseThrow(() -> new RuntimeException("User not found"));

        userRepository.delete(user);
    }

    public ApplicationUser updateVolunteerStatus(Integer userId, VolunteerStatus status) {
        ApplicationUser user = userRepository.findById(userId)
                .orElseThrow(() -> new RuntimeException("User not found"));

        user.setVolunteerStatus(status);
        return userRepository.save(user);
    }

    public ApplicationUser assignRolesToUser(Integer userId, Set<String> roleNames) {
        ApplicationUser user = userRepository.findById(userId)
                .orElseThrow(() -> new RuntimeException("User not found"));

        Set<Role> roles = new HashSet<>();
        for (String roleName : roleNames) {
            Role role = roleRepository.findByAuthority(roleName)
                    .orElseThrow(() -> new RuntimeException("Role not found: " + roleName));
            roles.add(role);
        }

        user.setAuthorities(roles);
        return userRepository.save(user);
    }

    @Transactional
    public ApplicationUser deactivateUserAccount(Integer userId, String reason) {
        ApplicationUser user = userRepository.findById(userId)
                .orElseThrow(() -> new RuntimeException("User not found"));

        user.setActive(false);
        user.setDeactivationReason(reason);

        return userRepository.save(user);
    }

    @Transactional
    public ApplicationUser reactivateUserAccount(Integer userId) {
        ApplicationUser user = userRepository.findById(userId)
                .orElseThrow(() -> new RuntimeException("User not found"));

        user.setActive(true);
        user.setDeactivationReason(null);

        return userRepository.save(user);
    }

    @Transactional
    public ApplicationUser selfDeactivateAccount(String username, String reason) {
        ApplicationUser user = userRepository.findByUsername(username)
                .orElseThrow(() -> new RuntimeException("User not found"));

        user.setActive(false);
        user.setDeactivationReason(reason);

        return userRepository.save(user);
    }
}