package ru.ulstu.user.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Sort;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import ru.ulstu.configuration.ApplicationProperties;
import ru.ulstu.core.error.EntityIdIsNullException;
import ru.ulstu.core.jpa.OffsetablePageRequest;
import ru.ulstu.core.model.BaseEntity;
import ru.ulstu.core.model.response.PageableItems;
import ru.ulstu.user.error.*;
import ru.ulstu.user.model.*;
import ru.ulstu.user.repository.UserRepository;
import ru.ulstu.user.repository.UserRoleRepository;
import ru.ulstu.user.util.UserUtils;

import java.util.*;
import java.util.stream.Collectors;

@Service
@Transactional
public class UserService implements UserDetailsService {
    private final Logger log = LoggerFactory.getLogger(UserService.class);
    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final UserRoleRepository userRoleRepository;
    private final UserMapper userMapper;
    private final MailService mailService;
    private final ApplicationProperties applicationProperties;

    public UserService(UserRepository userRepository,
                       PasswordEncoder passwordEncoder,
                       UserRoleRepository userRoleRepository,
                       UserMapper userMapper,
                       MailService mailService,
                       ApplicationProperties applicationProperties) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.userRoleRepository = userRoleRepository;
        this.userMapper = userMapper;
        this.mailService = mailService;
        this.applicationProperties = applicationProperties;
    }

    private User getUserByEmail(String email) {
        return userRepository.findOneByEmailIgnoreCase(email);
    }

    private User getUserByActivationKey(String activationKey) {
        return userRepository.findOneByActivationKey(activationKey);
    }

    public User getUserByLogin(String login) {
        return userRepository.findOneByLoginIgnoreCase(login);
    }

    @Transactional(readOnly = true)
    public UserDto getUserWithRolesById(Integer userId) {
        final User userEntity = userRepository.findOneWithRolesById(userId);
        if (userEntity == null) {
            throw new UserNotFoundException(userId.toString());
        }
        return userMapper.userEntityToUserDto(userEntity);
    }

    @Transactional(readOnly = true)
    public PageableItems<UserListDto> getAllUsers(int offset, int count) {
        final Page<User> page = userRepository.findAll(new OffsetablePageRequest(offset, count, new Sort("id")));
        return new PageableItems<>(page.getTotalElements(), userMapper.userEntitiesToUserListDtos(page.getContent()));
    }

    @Transactional(readOnly = true)
    public PageableItems<UserRoleDto> getUserRoles() {
        final List<UserRoleDto> roles = userRoleRepository.findAll().stream()
                .map(UserRoleDto::new)
                .sorted(Comparator.comparing(UserRoleDto::getViewValue))
                .collect(Collectors.toList());
        return new PageableItems<>(roles.size(), roles);
    }

    public UserDto createUser(UserDto userDto) {
        if (userDto.getId() != null) {
            throw new UserIdExistsException();
        }
        if (getUserByLogin(userDto.getLogin()) != null) {
            throw new UserLoginExistsException(userDto.getLogin());
        }
        if (getUserByEmail(userDto.getEmail()) != null) {
            throw new UserEmailExistsException(userDto.getEmail());
        }
        if (!userDto.isPasswordsValid()) {
            throw new UserPasswordsNotValidOrNotMatchException();
        }
        User user = userMapper.userDtoToUserEntity(userDto);
        user.setActivated(false);
        user.setActivationKey(UserUtils.generateActivationKey());
        user.setRoles(Collections.singleton(new UserRole(UserRoleConstants.USER)));
        user.setPassword(passwordEncoder.encode(userDto.getPassword()));
        user = userRepository.save(user);
        mailService.sendActivationEmail(user);
        log.debug("Created Information for User: {}", user.getLogin());
        return userMapper.userEntityToUserDto(user);
    }

    public UserDto activateUser(String activationKey) {
        final User user = getUserByActivationKey(activationKey);
        if (user == null) {
            throw new UserActivationError(activationKey);
        }
        user.setActivated(true);
        user.setActivationKey(null);
        user.setActivationDate(null);
        log.debug("Activated user: {}", user.getLogin());
        return userMapper.userEntityToUserDto(userRepository.save(user));
    }

    public UserDto updateUser(UserDto userDto) {
        if (userDto.getId() == null) {
            throw new EntityIdIsNullException();
        }
        if (!Objects.equals(
                Optional.ofNullable(getUserByEmail(userDto.getEmail()))
                        .map(BaseEntity::getId).orElse(userDto.getId()),
                userDto.getId())) {
            throw new UserEmailExistsException(userDto.getEmail());
        }
        if (!Objects.equals(
                Optional.ofNullable(getUserByLogin(userDto.getLogin()))
                        .map(BaseEntity::getId).orElse(userDto.getId()),
                userDto.getId())) {
            throw new UserLoginExistsException(userDto.getLogin());
        }
        User user = userRepository.findOne(userDto.getId());
        if (user == null) {
            throw new UserNotFoundException(userDto.getId().toString());
        }
        if (applicationProperties.getUndeadUserLogin().equalsIgnoreCase(user.getLogin())) {
            userDto.setLogin(applicationProperties.getUndeadUserLogin());
            userDto.setActivated(true);
            userDto.setRoles(Collections.singletonList(new UserRoleDto(UserRoleConstants.ADMIN)));
        }
        user.setLogin(userDto.getLogin());
        user.setFirstName(userDto.getFirstName());
        user.setLastName(userDto.getLastName());
        user.setEmail(userDto.getEmail());
        if (userDto.isActivated() != user.getActivated()) {
            if (userDto.isActivated()) {
                user.setActivationKey(null);
                user.setActivationDate(null);
            } else {
                user.setActivationKey(UserUtils.generateActivationKey());
                user.setActivationDate(new Date());
            }
        }
        user.setActivated(userDto.isActivated());
        final Set<UserRole> roles = userMapper.rolesFromDto(userDto.getRoles());
        user.setRoles(roles.isEmpty()
                ? Collections.singleton(new UserRole(UserRoleConstants.USER))
                : roles);
        if (!StringUtils.isEmpty(userDto.getOldPassword())) {
            if (!userDto.isPasswordsValid() || !userDto.isOldPasswordValid()) {
                throw new UserPasswordsNotValidOrNotMatchException();
            }
            if (!passwordEncoder.matches(userDto.getOldPassword(), user.getPassword())) {
                throw new UserPasswordsNotValidOrNotMatchException();
            }
            user.setPassword(passwordEncoder.encode(userDto.getPassword()));
            log.debug("Changed password for User: {}", user.getLogin());
        }
        user = userRepository.save(user);
        log.debug("Changed Information for User: {}", user.getLogin());
        return userMapper.userEntityToUserDto(user);
    }

    public UserDto updateUserInformation(UserDto userDto) {
        if (userDto.getId() == null) {
            throw new EntityIdIsNullException();
        }
        if (!Objects.equals(
                Optional.ofNullable(getUserByEmail(userDto.getEmail()))
                        .map(BaseEntity::getId).orElse(userDto.getId()),
                userDto.getId())) {
            throw new UserEmailExistsException(userDto.getEmail());
        }
        User user = userRepository.findOne(userDto.getId());
        if (user == null) {
            throw new UserNotFoundException(userDto.getId().toString());
        }
        user.setFirstName(userDto.getFirstName());
        user.setLastName(userDto.getLastName());
        user.setEmail(userDto.getEmail());
        user = userRepository.save(user);
        log.debug("Updated Information for User: {}", user.getLogin());
        return userMapper.userEntityToUserDto(user);
    }

    public UserDto changeUserPassword(UserDto userDto) {
        if (userDto.getId() == null) {
            throw new EntityIdIsNullException();
        }
        if (!userDto.isPasswordsValid() || !userDto.isOldPasswordValid()) {
            throw new UserPasswordsNotValidOrNotMatchException();
        }
        final String login = UserUtils.getCurrentUserLogin();
        final User user = userRepository.findOneByLoginIgnoreCase(login);
        if (user == null) {
            throw new UserNotFoundException(login);
        }
        if (!passwordEncoder.matches(userDto.getOldPassword(), user.getPassword())) {
            throw new UserPasswordsNotValidOrNotMatchException();
        }
        user.setPassword(passwordEncoder.encode(userDto.getPassword()));
        log.debug("Changed password for User: {}", user.getLogin());
        return userMapper.userEntityToUserDto(userRepository.save(user));
    }

    public boolean requestUserPasswordReset(String email) {
        User user = userRepository.findOneByEmailIgnoreCase(email);
        if (user == null) {
            throw new UserNotFoundException(email);
        }
        if (!user.getActivated()) {
            throw new UserNotActivatedException();
        }
        user.setResetKey(UserUtils.generateResetKey());
        user.setResetDate(new Date());
        user = userRepository.save(user);
        mailService.sendPasswordResetMail(user);
        log.debug("Created Reset Password Request for User: {}", user.getLogin());
        return true;
    }

    public boolean completeUserPasswordReset(String key, UserResetPasswordDto userResetPasswordDto) {
        if (!userResetPasswordDto.isPasswordsValid()) {
            throw new UserPasswordsNotValidOrNotMatchException();
        }
        User user = userRepository.findOneByResetKey(key);
        if (user == null) {
            throw new UserResetKeyError(key);
        }
        user.setPassword(passwordEncoder.encode(userResetPasswordDto.getPassword()));
        user.setResetKey(null);
        user.setResetDate(null);
        user = userRepository.save(user);
        log.debug("Reset Password for User: {}", user.getLogin());
        return true;
    }

    public UserDto deleteUser(Integer userId) {
        final User user = userRepository.findOne(userId);
        if (user == null) {
            throw new UserNotFoundException(userId.toString());
        }
        if (applicationProperties.getUndeadUserLogin().equalsIgnoreCase(user.getLogin())) {
            throw new UserIsUndeadException(user.getLogin());
        }
        userRepository.delete(user);
        log.debug("Deleted User: {}", user.getLogin());
        return userMapper.userEntityToUserDto(user);
    }

    @Override
    public UserDetails loadUserByUsername(String username) {
        final User user = userRepository.findOneByLoginIgnoreCase(username);
        if (user == null) {
            throw new UserNotFoundException(username);
        }
        if (!user.getActivated()) {
            throw new UserNotActivatedException();
        }
        return new org.springframework.security.core.userdetails.User(user.getLogin(),
                user.getPassword(),
                Optional.ofNullable(user.getRoles()).orElse(Collections.emptySet()).stream()
                        .map(role -> new SimpleGrantedAuthority(role.getName()))
                        .collect(Collectors.toList()));
    }

    public List<User> findByIds(List<Integer> ids) {
        return userRepository.findAll(ids);
    }
}
