package com.utopiaindustries.service;

import com.utopiaindustries.dao.ctp.AuthorityDAO;
import com.utopiaindustries.dao.ctp.InventoryAccountDAO;
import com.utopiaindustries.dao.ctp.UserDAO;
import com.utopiaindustries.dao.ctp.UserInventoryAccountDAO;
import com.utopiaindustries.model.ctp.Authority;
import com.utopiaindustries.model.ctp.InventoryAccount;
import com.utopiaindustries.model.ctp.User;
import com.utopiaindustries.model.ctp.UserInventoryAccount;
import com.utopiaindustries.util.StringUtils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
public class UserService {

    private final JdbcUserDetailsManager jdbcUserDetailsManager;
    private final UserDAO userDAO;
    private final AuthorityDAO authorityDAO;
    private final UserInventoryAccountDAO userInventoryAccountDAO;
    private final InventoryAccountDAO inventoryAccountDAO;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;


    public UserService(JdbcUserDetailsManager jdbcUserDetailsManager, UserDAO userDAO, AuthorityDAO authorityDAO, UserInventoryAccountDAO userInventoryAccountDAO, InventoryAccountDAO inventoryAccountDAO, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.jdbcUserDetailsManager = jdbcUserDetailsManager;
        this.userDAO = userDAO;
        this.authorityDAO = authorityDAO;
        this.userInventoryAccountDAO = userInventoryAccountDAO;
        this.inventoryAccountDAO = inventoryAccountDAO;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    public User createEmptyUser(){
        User user = new User();
        user.setAuthorities( new ArrayList<>() );
        user.setInventoryAccounts( new ArrayList<>() );
        user.setEnabled( true );
        return user;
    }

    public List<User> findAllUsers(){
        List<User> users =  userDAO.findAll();
        populateRolesAndInventoryAccountInUsers( users );
        return users;
    }

    /*
    * get user by username
    * */
    public  User getUser( String username ){
        User user = userDAO.findByUsername( username );
        populateRolesAndInventoryAccountInUsers( Collections.singletonList( user ) );
        return user;
    }

    /*
    * save user
    * */
    @Transactional
    public void saveUser( User user, boolean checkNewOrEdit ){
        if(userDAO.findByUsername(user.getUsername()) != null && checkNewOrEdit ){
            throw new RuntimeException("username already exists");
        }
        // save user
        createUser( user );
        // remove previous
        deletePreviousRolesAndAccounts( user );
        // save again
        saveUserRolesAndAccounts( user );
    }


    private void createUser( User user ){
        User newUser = new User();
        newUser.setPassword(  user.getPassword()  );
        if( ! StringUtils.isNullOrEmpty( user.getNewPassword() )){
            newUser.setPassword( bCryptPasswordEncoder.encode( user.getNewPassword() ) );
        }
        newUser.setEnabled( user.getEnabled() );
        newUser.setUsername( user.getUsername() );
        userDAO.save( newUser );
    }

    private void saveUserRolesAndAccounts( User user ){
        List<Authority> newRoles = user.getAuthorities();
        if(newRoles != null) {
            newRoles.forEach(authority -> authority.setUsername(user.getUsername()));
            authorityDAO.saveAll( newRoles );
        }
        List<UserInventoryAccount> newInventoryAccounts = user.getInventoryAccounts();
        if( newInventoryAccounts != null ){
            newInventoryAccounts.forEach(account -> account.setUsername( user.getUsername() ));
            userInventoryAccountDAO.saveAll( newInventoryAccounts );
        }
    }

    private void deletePreviousRolesAndAccounts( User user ){
        List<Authority> oldRoles = authorityDAO.findByUsername( user.getUsername() );
        // delete
        authorityDAO.deleteByUsername( user.getUsername() );
        userInventoryAccountDAO.deleteByUsername( user.getUsername() );
    }

    /*
    *  populate wrapper
    * */
    private void populateRolesAndInventoryAccountInUsers( List<User> users ){
        if( users != null && ! users.isEmpty() ){
            // maps
            Map<String , List<UserInventoryAccount>> userInvMap = new HashMap<>();
            Map<String, List<Authority>> userAuthMap = new HashMap<>();

            List<String> usernames = users.stream()
                    .map( User::getUsername )
                    .collect( Collectors.toList() );
            // get accounts
            List<UserInventoryAccount> inventoryAccounts = userInventoryAccountDAO.findByUsernames( usernames );
            {
                // setting titles on inventory accounts
                if (!inventoryAccounts.isEmpty()) {
                    List<Long> accountIds = inventoryAccounts
                            .stream()
                            .map(UserInventoryAccount::getAccountId)
                            .collect(Collectors.toList());
                    List<InventoryAccount> accounts = inventoryAccountDAO.findByIds(accountIds);
                    if (!accounts.isEmpty()) {
                        Map<Long, String> accToTitleMap = accounts.stream()
                                .collect(Collectors.toMap(InventoryAccount::getId, InventoryAccount::getTitle));
                        for (UserInventoryAccount userInventoryAccount : inventoryAccounts) {
                            userInventoryAccount.setAccountTitle( accToTitleMap.getOrDefault(userInventoryAccount.getAccountId(), ""));
                        }
                    }
                }
            }

            // get roles
            List<Authority> authorities = authorityDAO.findByUsernames( usernames );
            if( ! inventoryAccounts.isEmpty() ){
                userInvMap = inventoryAccounts
                        .stream()
                        .collect( Collectors.groupingBy( UserInventoryAccount::getUsername ) );
            }
            if( ! authorities.isEmpty() ){
                userAuthMap = authorities
                        .stream()
                        .collect( Collectors.groupingBy( Authority::getUsername ) );
            }
            // set the fields
            for( User user : users ){
                user.setInventoryAccounts( userInvMap.getOrDefault( user.getUsername() , new ArrayList<>())  );
                user.setAuthorities( userAuthMap.getOrDefault( user.getUsername() , new ArrayList<>())  );
            }
        }
    }
}
