package vn.azteam.tpf.service;

import com.google.common.collect.Lists;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.web.multipart.MultipartFile;
import vn.azteam.tpf.config.Constants;
import vn.azteam.tpf.domain.*;
import vn.azteam.tpf.repository.*;
import vn.azteam.tpf.repository.search.UserSearchRepository;
import vn.azteam.tpf.security.AuthoritiesConstants;
import vn.azteam.tpf.security.SecurityUtils;
import vn.azteam.tpf.service.dto.TBRoleDTO;
import vn.azteam.tpf.service.dto.UserDTO;
import vn.azteam.tpf.service.mapper.UserMapper;
import vn.azteam.tpf.service.util.RandomUtil;
import vn.azteam.tpf.web.rest.errors.*;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.swing.text.html.Option;
import java.math.BigInteger;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

/**
 * Service class for managing users.
 */
@Service
@Transactional
public class UserService {

    private static final String ENTITY_NAME = "User";
    private final Logger log = LoggerFactory.getLogger(UserService.class);

    private final UserRepository userRepository;

    private final UserMapper userMapper;

    private final TBDetailUserRepository tbDetailUserRepository;

    private final PasswordEncoder passwordEncoder;

    private final UserSearchRepository userSearchRepository;

    private final AuthorityRepository authorityRepository;

    private final TBAddressRepository tbAddressRepository;

    private final CacheManager cacheManager;

    private final EntityManager em;

    private final FileStorageService fileStorageService;

    private final TBRoleRepository tbRoleRepository;

    public UserService(UserRepository userRepository
        , UserMapper userMapper
        , TBDetailUserRepository tbDetailUserRepository
        , PasswordEncoder passwordEncoder
        , UserSearchRepository userSearchRepository
        , AuthorityRepository authorityRepository
        , TBAddressRepository tbAddressRepository
        , CacheManager cacheManager
        , EntityManager em
        , FileStorageService fileStorageService, TBRoleRepository tbRoleRepository) {
        this.userRepository = userRepository;
        this.userMapper = userMapper;
        this.tbDetailUserRepository = tbDetailUserRepository;
        this.passwordEncoder = passwordEncoder;
        this.userSearchRepository = userSearchRepository;
        this.authorityRepository = authorityRepository;
        this.tbAddressRepository = tbAddressRepository;
        this.cacheManager = cacheManager;
        this.em = em;
        this.fileStorageService = fileStorageService;
        this.tbRoleRepository = tbRoleRepository;
    }

    public Optional<User> activateRegistration(String key) {
        log.debug("Activating user for activation key {}", key);
        return userRepository.findOneByActivationKey(key)
            .map(user -> {
                // activate given user for the registration key.
                user.setActivated(true);
                user.setActivationKey(null);
                userSearchRepository.save(user);
                this.clearUserCaches(user);
                log.debug("Activated user: {}", user);
                return user;
            });
    }

    public Optional<User> completePasswordReset(String newPassword, String key) {
        log.debug("Reset user password for reset key {}", key);
        return userRepository.findOneByResetKey(key)
            .filter(user -> user.getResetDate().isAfter(Instant.now().minusSeconds(86400)))
            .map(user -> {
                user.setPassword(passwordEncoder.encode(newPassword));
                user.setResetKey(null);
                user.setResetDate(null);
                this.clearUserCaches(user);
                return user;
            });
    }

    public Optional<User> requestPasswordReset(String mail) {
        return userRepository.findOneByEmailIgnoreCase(mail)
            .filter(User::getActivated)
            .map(user -> {
                user.setResetKey(RandomUtil.generateResetKey());
                user.setResetDate(Instant.now());
                this.clearUserCaches(user);
                return user;
            });
    }

    public User registerUser(UserDTO userDTO, String password) {
        userRepository.findOneByLogin(userDTO.getLogin().toLowerCase()).ifPresent(existingUser -> {
            boolean removed = removeNonActivatedUser(existingUser);
            if (!removed) {
                throw new LoginAlreadyUsedException();
            }
        });
        userRepository.findOneByEmailIgnoreCase(userDTO.getEmail()).ifPresent(existingUser -> {
            boolean removed = removeNonActivatedUser(existingUser);
            if (!removed) {
                throw new EmailAlreadyUsedException();
            }
        });
        User newUser = new User();
        String encryptedPassword = passwordEncoder.encode(password);
        newUser.setLogin(userDTO.getLogin().toLowerCase());
        // new user gets initially a generated password
        newUser.setPassword(encryptedPassword);
        newUser.setFirstName(userDTO.getFirstName());
        newUser.setLastName(userDTO.getLastName());
        newUser.setEmail(userDTO.getEmail().toLowerCase());
        newUser.setImageUrl(userDTO.getImageUrl());
        newUser.setLangKey(userDTO.getLangKey());
        // new user is not active
        newUser.setActivated(false);
        // new user gets registration key
        newUser.setActivationKey(RandomUtil.generateActivationKey());
        Set<Authority> authorities = new HashSet<>();
        authorityRepository.findById(AuthoritiesConstants.USER).ifPresent(authorities::add);
        newUser.setAuthorities(authorities);
        userRepository.save(newUser);
        userSearchRepository.save(newUser);
        this.clearUserCaches(newUser);
        log.debug("Created Information for User: {}", newUser);

        TBDetailUser tbDetailUser = new TBDetailUser();
        tbDetailUser.setFullName(userDTO.getFullName());
        tbDetailUser.setPhone(userDTO.getPhone());
        tbDetailUser.setGender(userDTO.getGender());
        tbDetailUser.setBirthday(userDTO.getBirthDay());
        if(null != userDTO.getRoleId()){
            TBRole tbRole = em.getReference(TBRole.class, userDTO.getRoleId());
            tbDetailUser.setTbRole(tbRole);
        }
        if(null != userDTO.getCustomerId()){
            TBCustomer tbCustomer = em.getReference(TBCustomer.class, userDTO.getCustomerId());
            tbDetailUser.setTbCustomer(tbCustomer);
        }
        if(null != userDTO.getAddressId()){
            TBAddress tbAddress = em.getReference(TBAddress.class, userDTO.getAddressId());
            tbDetailUser.setTbAddress(tbAddress);
        }

        tbDetailUser.setUser(newUser);
        tbDetailUserRepository.save(tbDetailUser);
        newUser.setTBDetailUser(tbDetailUser);
        userSearchRepository.save(newUser);

        return newUser;
    }

    private boolean removeNonActivatedUser(User existingUser){
        if (existingUser.getActivated()) {
            return false;
        }
        userRepository.delete(existingUser);
        userRepository.flush();
        this.clearUserCaches(existingUser);
        return true;
    }

    /**
     * Get user RoleId
     * @return
     */
    public Optional<UserDTO> getCurrentUserDTO() {
        return getUserWithAuthorities().map(UserDTO::new);
    }

    public List<Long> getListRoleAllCustomer (){
        String query = "SELECT DISTINCT (r.id) FROM tb_role as r\n" +
            "where r.is_all_customer = 1";
        Query envStandardQuery = em.createNativeQuery(query);
        List<BigInteger> cropIds = envStandardQuery.getResultList();

        List<Long> finalResult = new ArrayList<>();
        for(BigInteger cropId : cropIds ) {
            finalResult.add(cropId.longValue());
        }
        return finalResult;
    }

    public boolean isAllCustomerRole(Long roleId) {
        return getListRoleAllCustomer().contains(roleId);
    }

    public List<Long> getListRoleAllCrop (){
        String query = "SELECT DISTINCT (r.id) FROM tb_role as r\n" +
            "where r.is_all_crop = 1";
        Query envStandardQuery = em.createNativeQuery(query);
        List<BigInteger> cropIds = envStandardQuery.getResultList();

        List<Long> finalResult = new ArrayList<>();
        for(BigInteger cropId : cropIds ) {
            finalResult.add(cropId.longValue());
        }
        return finalResult;
    }

    public User createUser(UserDTO userDTO) {
        User user = new User();
        UserDTO currentUserDTO = getCurrentUserDTO().get();
        if(currentUserDTO.getCustomerId() != null && userDTO.getCustomerId() != currentUserDTO.getCustomerId()){
            throw new BadRequestAlertException("1019", ENTITY_NAME, "1019.");
        }
        user.setLogin(userDTO.getLogin().toLowerCase());
        user.setFirstName(userDTO.getFirstName());
        user.setLastName(userDTO.getLastName());
        user.setEmail(userDTO.getEmail().toLowerCase());
        user.setImageUrl(userDTO.getImageUrl());
        if (userDTO.getLangKey() == null) {
            user.setLangKey(Constants.DEFAULT_LANGUAGE); // default language
        } else {
            user.setLangKey(userDTO.getLangKey());
        }
        String encryptedPassword = passwordEncoder.encode(RandomUtil.generatePassword());
        user.setPassword(encryptedPassword);
        user.setResetKey(RandomUtil.generateResetKey());
        user.setResetDate(Instant.now());
        user.setActivated(true);

        if (userDTO.getAuthorities() == null) {
            userDTO.setAuthorities(new HashSet<>(Arrays.asList(AuthoritiesConstants.USER)));
        }
        Set<Authority> authorities = userDTO.getAuthorities().stream()
            .map(authorityRepository::findById)
            .filter(Optional::isPresent)
            .map(Optional::get)
            .collect(Collectors.toSet());
        user.setAuthorities(authorities);

        User result = userRepository.save(user);
        userSearchRepository.save(result);
        log.debug("Created Information for User: {}", user);

        TBDetailUser tbDetailUser = new TBDetailUser();
        tbDetailUser.setId(result.getId());
        tbDetailUser.setFullName(userDTO.getFullName());
        tbDetailUser.setPhone(userDTO.getPhone());
        tbDetailUser.setGender(userDTO.getGender());
        tbDetailUser.setBirthday(userDTO.getBirthDay());
        if(null != userDTO.getRoleId()){
            //Check role of current user
            Optional<TBRole> currentRoleOptional = tbRoleRepository.findById(currentUserDTO.getRoleId());
            if(!currentRoleOptional.isPresent()) {
                throw new BadRequestAlertException("1023", ENTITY_NAME, "1023");
            }

            Optional<TBRole> roleOptional = tbRoleRepository.findById(userDTO.getRoleId());
            if(!roleOptional.isPresent()) {
                throw new BadRequestAlertException("1024", ENTITY_NAME, "1024");
            }

            if(currentRoleOptional.get().getRoleLevel() > roleOptional.get().getRoleLevel()) {
                throw new BadRequestAlertException("1026", ENTITY_NAME, "1026");
            }

            TBRole tbRole = em.getReference(TBRole.class, userDTO.getRoleId());
            tbDetailUser.setTbRole(tbRole);
        }

        if(currentUserDTO.getCustomerId() != null) {
            TBCustomer tbCustomer = em.getReference(TBCustomer.class, currentUserDTO.getCustomerId());
            tbDetailUser.setTbCustomer(tbCustomer);
        }
        else if (userDTO.getCustomerId() !=  null) {
            TBCustomer tbCustomer = em.getReference(TBCustomer.class, userDTO.getCustomerId());
            tbDetailUser.setTbCustomer(tbCustomer);
        }
        else {
            tbDetailUser.setTbCustomer(null);
        }

        TBAddress tbAddress = new TBAddress();
        if(null != userDTO.getAddressId()){
            tbAddress = em.getReference(TBAddress.class, userDTO.getAddressId());
        }

        //Create or update TBAddress
        if(null != userDTO.getCountryId()){
            TBCountry tbCountry = em.getReference(TBCountry.class, userDTO.getCountryId());
            tbAddress.setTbCountry(tbCountry);
        }

        if(null != userDTO.getCityId()){
            TBCity tbCity = em.getReference(TBCity.class, userDTO.getCityId());
            tbAddress.setTbCity(tbCity);
        }

        if(null != userDTO.getDistrictId()){
            TBDistrict tbDistrict = em.getReference(TBDistrict.class, userDTO.getDistrictId());
            tbAddress.setTbDistrict(tbDistrict);
        }

        if(null != userDTO.getWardId()){
            TBWard tbWard = em.getReference(TBWard.class, userDTO.getWardId());
            tbAddress.setTbWard(tbWard);
        }

        tbAddress.setAddress(userDTO.getAddress());
        TBAddress savedTBAddress = tbAddressRepository.save(tbAddress);

        tbDetailUser.setTbAddress(savedTBAddress);
        tbDetailUser.setUser(user);
        tbDetailUserRepository.save(tbDetailUser);
        user.setTBDetailUser(tbDetailUser);
        userSearchRepository.save(user);
        this.clearUserCaches(user);
        log.debug("Created Information for TBDetailUser: {}", tbDetailUser);
        return user;
    }

    /**
     * Update basic information (first name, last name, email, language) for the current user.
     *
     * @param firstName first name of user
     * @param lastName last name of user
     * @param email email id of user
     * @param langKey language key
     * @param imageUrl image URL of user
     */
    public void updateUser(String firstName, String lastName, String email, String langKey, String imageUrl) {
        SecurityUtils.getCurrentUserLogin()
            .flatMap(userRepository::findOneByLogin)
            .ifPresent(user -> {
                user.setFirstName(firstName);
                user.setLastName(lastName);
                user.setEmail(email.toLowerCase());
                user.setLangKey(langKey);
                user.setImageUrl(imageUrl);
                userSearchRepository.save(user);
                this.clearUserCaches(user);
                log.debug("Changed Information for User: {}", user);
            });
    }

    /**
     * Update all information for a specific user, and return the modified user.
     *
     * @param userDTO user to update
     * @return updated user
     */
    public Optional<UserDTO> updateUser(UserDTO userDTO) {
        UserDTO currentUserDTO = getCurrentUserDTO().get();
        if(currentUserDTO.getCustomerId() != null && !userDTO.getCustomerId().equals(currentUserDTO.getCustomerId())){
            throw new BadRequestAlertException("1019", ENTITY_NAME, "1019");
        }
        return Optional.of(userRepository
            .findById(userDTO.getId()))
            .filter(Optional::isPresent)
            .map(Optional::get)
            .map(user -> {
                this.clearUserCaches(user);
                user.setLogin(userDTO.getLogin().toLowerCase());
                user.setFirstName(userDTO.getFirstName());
                user.setLastName(userDTO.getLastName());
                user.setEmail(userDTO.getEmail().toLowerCase());
                user.setImageUrl(userDTO.getImageUrl());
                user.setActivated(userDTO.isActivated());
                user.setLangKey(userDTO.getLangKey());
                Set<Authority> managedAuthorities = user.getAuthorities();
                managedAuthorities.clear();
                userDTO.getAuthorities().stream()
                    .map(authorityRepository::findById)
                    .filter(Optional::isPresent)
                    .map(Optional::get)
                    .forEach(managedAuthorities::add);
                this.clearUserCaches(user);
                log.debug("Changed Information for User: {}", user);
                return user;
            })
            .map(user -> tbDetailUserRepository.findById(user.getId()))
            .filter(Optional::isPresent)
            .map(Optional::get)
            .map(tbDetailUser -> {
                tbDetailUser.setFullName(userDTO.getFullName());
                tbDetailUser.setPhone(userDTO.getPhone());
                tbDetailUser.setBirthday(userDTO.getBirthDay());
                tbDetailUser.setGender(userDTO.getGender());
                tbDetailUser.setFcmToken(userDTO.getFcmToken());
                if(null != userDTO.getRoleId()){
                    //Check role of current user
                    Optional<TBRole> currentRoleOptional = tbRoleRepository.findById(currentUserDTO.getRoleId());
                    if(!currentRoleOptional.isPresent()) {
                        throw new BadRequestAlertException("1023", ENTITY_NAME, "1023");
                    }

                    Optional<TBRole> roleOptional = tbRoleRepository.findById(userDTO.getRoleId());
                    if(!roleOptional.isPresent()) {
                        throw new BadRequestAlertException("1025", ENTITY_NAME, "1025");
                    }

                    if(currentRoleOptional.get().getRoleLevel() > roleOptional.get().getRoleLevel()) {
                        throw new BadRequestAlertException("1027", ENTITY_NAME, "1027");
                    }

                    TBRole tbRole = em.getReference(TBRole.class, userDTO.getRoleId());
                    tbDetailUser.setTbRole(tbRole);
                }

                if(currentUserDTO.getCustomerId() != null) {
                    TBCustomer tbCustomer = em.getReference(TBCustomer.class, currentUserDTO.getCustomerId());
                    tbDetailUser.setTbCustomer(tbCustomer);
                }
                else if (userDTO.getCustomerId() !=  null) {
                    TBCustomer tbCustomer = em.getReference(TBCustomer.class, userDTO.getCustomerId());
                    tbDetailUser.setTbCustomer(tbCustomer);
                }
                else {
                    tbDetailUser.setTbCustomer(null);
                }

                TBAddress tbAddress;
                if(null != userDTO.getAddressId()){
                    tbAddress = em.getReference(TBAddress.class, userDTO.getAddressId());
                } else if(currentUserDTO.getAddressId() != null) {
                    tbAddress = em.getReference(TBAddress.class, currentUserDTO.getAddressId());
                } else {
                    tbAddress = new TBAddress();
                }

                //Create or update TBAddress
                if(null != userDTO.getCountryId()){
                    TBCountry tbCountry = em.getReference(TBCountry.class, userDTO.getCountryId());
                    tbAddress.setTbCountry(tbCountry);
                }

                if(null != userDTO.getCityId()){
                    TBCity tbCity = em.getReference(TBCity.class, userDTO.getCityId());
                    tbAddress.setTbCity(tbCity);
                }

                if(null != userDTO.getDistrictId()){
                    TBDistrict tbDistrict = em.getReference(TBDistrict.class, userDTO.getDistrictId());
                    tbAddress.setTbDistrict(tbDistrict);
                }

                if(null != userDTO.getWardId()){
                    TBWard tbWard = em.getReference(TBWard.class, userDTO.getWardId());
                    tbAddress.setTbWard(tbWard);
                }

                tbAddress.setAddress(userDTO.getAddress());
                TBAddress savedTBAddress = tbAddressRepository.save(tbAddress);

                tbDetailUser.setTbAddress(savedTBAddress);

                log.debug("Changed Information for TBDetailUser: {}", tbDetailUser);
                return tbDetailUser.getUser();
            })
            .map(user -> {
                userSearchRepository.save(user);
                this.clearUserCaches(user);
                return user;
            })
            .map(UserDTO::new);
    }

    public void deleteUser(String login) {
        userRepository.findOneByLogin(login).ifPresent(user -> {
            userRepository.delete(user);
            userSearchRepository.delete(user);
            this.clearUserCaches(user);
            log.debug("Deleted User: {}", user);
        });
    }

    public void changePassword(String currentClearTextPassword, String newPassword) {
        SecurityUtils.getCurrentUserLogin()
            .flatMap(userRepository::findOneByLogin)
            .ifPresent(user -> {
                String currentEncryptedPassword = user.getPassword();
                if (!passwordEncoder.matches(currentClearTextPassword, currentEncryptedPassword)) {
                    throw new InvalidPasswordException();
                }
                String encryptedPassword = passwordEncoder.encode(newPassword);
                user.setPassword(encryptedPassword);
                this.clearUserCaches(user);
                log.debug("Changed password for User: {}", user);
            });
    }

    @Transactional(readOnly = true)
    public Page<UserDTO> getAllUsers(Pageable pageable) {
        return userRepository.findAll(pageable).map(UserDTO::new);
    }

    @Transactional(readOnly = true)
    public Page<UserDTO> getAllManagedUsers(Pageable pageable) {
        return userRepository.findAllByLoginNot(pageable, Constants.ANONYMOUS_USER).map(UserDTO::new);
    }


    @Transactional(readOnly = true)
    public Optional<User> getUserWithAuthoritiesByLogin(String login) {
        return userRepository.findOneWithAuthoritiesByLogin(login);
    }

    @Transactional(readOnly = true)
    public Optional<User> getUserWithAuthorities(Long id) {
        return userRepository.findOneWithAuthoritiesById(id);
    }

    @Transactional(readOnly = true)
    public Optional<User> getUserWithAuthorities() {
        return SecurityUtils.getCurrentUserLogin().flatMap(userRepository::findOneWithAuthoritiesByLogin);
    }

    /**
     * Not activated users should be automatically deleted after 3 days.
     * <p>
     * This is scheduled to get fired everyday, at 01:00 (am).
     */
    @Scheduled(cron = "0 0 1 * * ?")
    public void removeNotActivatedUsers() {
        userRepository
            .findAllByActivatedIsFalseAndCreatedDateBefore(Instant.now().minus(3, ChronoUnit.DAYS))
            .forEach(user -> {
                log.debug("Deleting not activated user {}", user.getLogin());
                userRepository.delete(user);
                userSearchRepository.delete(user);
                this.clearUserCaches(user);
            });
    }

    /**
     * @return a list of all the authorities
     */
    public List<String> getAuthorities() {
        return authorityRepository.findAll().stream().map(Authority::getName).collect(Collectors.toList());
    }

    private void clearUserCaches(User user) {
        Objects.requireNonNull(cacheManager.getCache(UserRepository.USERS_BY_LOGIN_CACHE)).evict(user.getLogin());
        Objects.requireNonNull(cacheManager.getCache(UserRepository.USERS_BY_EMAIL_CACHE)).evict(user.getEmail());
    }

    /**
     * Check exist user by email
     * @param emailSearch
     * @param pageable
     * @return
     */
    public boolean IsExistEmailUser(String emailSearch,Pageable pageable) {
        boolean isExist = false;
        Specification<User> specification = Specification.where(
            (root, criteriaQuery, criteriaBuilder) ->
                criteriaBuilder.equal(
                    root.get(User_.email),emailSearch
                )
        );
        Page<UserDTO>  lstUserDTO = userRepository.findAll(specification, pageable).map(UserDTO::new);
        if(lstUserDTO != null && lstUserDTO.getTotalElements() > 0)
        {
            isExist = true;
        }
        return isExist;
    }

    /**
     * Upload images of user.
     * @param userId
     * @param images
     * @return
     */
    @Transactional()
    public String uploadImages(Long userId, MultipartFile[] images){
        UserDTO userDTO = this.getUserWithAuthorities(userId).map(UserDTO::new).get();
        String imageUrl = null;
        if (null != images && images.length > 0) {
            imageUrl = this.fileStorageService.storeFiles("Avatar" + "_" + userDTO.getId().toString(), images);
        }
        return imageUrl;
    }

    public Page<UserDTO> searchUser(String query, Pageable pageable) {
        String sqlString = "select u.*\n" +
                            " from jhi_user as u\n" +
                            " join tb_detail_user as du on du.user_id = u.id\n" +
                            " join tb_role as r on du.tb_role_id = r. id\n" +
                            " where u.login like '%" + query + "%'\n" +
                            " or u.email like '%" + query + "%'\n" +
                            " or du.full_name like '%" + query + "%'\n" +
                            " or du.phone like '%" + query + "%'\n" +
                            " or r.name like '%" + query + "%'\n";
        Query userQuery = em.createNativeQuery(sqlString, User.class);
        List<UserDTO> userDTOs = userMapper.usersToUserDTOs(userQuery.getResultList());
        int start = Math.toIntExact(pageable.getOffset());
        int end = Math.toIntExact((start + pageable.getPageSize()) > userDTOs.size() ? userDTOs.size() : (start + pageable.getPageSize()));
        if(userDTOs.size() > start) {
            return new PageImpl<>(userDTOs.subList(start, end), pageable, userDTOs.size());
        }
        return new PageImpl<>(Lists.newArrayList(), pageable, userDTOs.size());
    }
}
