package eu.eudat.logic.managers;

import eu.eudat.data.entities.Credential;
import eu.eudat.data.entities.EmailConfirmation;
import eu.eudat.data.entities.UserDMP;
import eu.eudat.data.entities.UserInfo;
import eu.eudat.data.entities.UserToken;
import eu.eudat.exceptions.emailconfirmation.HasConfirmedEmailException;
import eu.eudat.exceptions.emailconfirmation.TokenExpiredException;
import eu.eudat.logic.services.ApiContext;
import eu.eudat.logic.services.operations.DatabaseRepository;
import eu.eudat.models.data.security.Principal;
import eu.eudat.queryable.QueryableList;
import eu.eudat.queryable.jpa.predicates.OrderByPredicate;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.transaction.Transactional;
import java.io.IOException;
import java.util.*;

@Component
public class MergeEmailConfirmationManager {
	private static Logger logger = LoggerFactory.getLogger(MergeEmailConfirmationManager.class);
	private ApiContext apiContext;
	private DatabaseRepository databaseRepository;

	@Autowired
	public MergeEmailConfirmationManager(ApiContext apiContext) {
		this.apiContext = apiContext;
		this.databaseRepository = apiContext.getOperationsContext().getDatabaseRepository();
	}

	@Transactional
	public void confirmEmail(String token) throws TokenExpiredException, HasConfirmedEmailException {
		EmailConfirmation loginConfirmationEmail = apiContext.getOperationsContext()
				.getDatabaseRepository().getLoginConfirmationEmailDao().asQueryable()
				.where((builder, root) -> builder.equal(root.get("token"), UUID.fromString(token))).getSingle();
		
		if (loginConfirmationEmail.getExpiresAt().compareTo(new Date()) < 0)
			throw new TokenExpiredException("Token has expired.");

		UserInfo userToBeMerged = databaseRepository.getUserInfoDao().asQueryable()
				.where((builder, root) -> builder.equal(root.get("id"), loginConfirmationEmail.getUserId())).getSingle();
		
		try {
			Map<String, Object> map = new ObjectMapper().readValue(loginConfirmationEmail.getData(), HashMap.class);
			UUID otherUserId = UUID.fromString((String) map.get("userId"));
			UserInfo user = databaseRepository.getUserInfoDao().asQueryable()
					.where((builder, root) -> builder.equal(root.get("id"), otherUserId)).getSingle();

			// Checks if mail is used by another user. If it is, merges the new the old.
			mergeNewUserToOld(user, userToBeMerged, Integer.valueOf((String) map.get("provider")));
			expireUserToken(userToBeMerged);
			loginConfirmationEmail.setIsConfirmed(true);
			databaseRepository.getLoginConfirmationEmailDao().createOrUpdate(loginConfirmationEmail);
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
		}
	}

	public void sendConfirmationEmail(String email, Principal principal, UUID userId, Integer provider) throws HasConfirmedEmailException {
		UserInfo user = apiContext.getOperationsContext().getDatabaseRepository().getUserInfoDao().find(principal.getId());

		if (user.getEmail() != null && !user.getEmail().equals(email)) {
			apiContext.getUtilitiesService().getConfirmationEmailService().createMergeConfirmationEmail(
					databaseRepository.getLoginConfirmationEmailDao(),
					apiContext.getUtilitiesService().getMailService(),
					email,
					userId,
					principal,
					provider
			);
		}
	}

	@Transactional
	private void mergeNewUserToOld(UserInfo newUser, UserInfo oldUser, Integer provider) {
		Credential credential = databaseRepository.getCredentialDao().asQueryable().where((builder, root) -> builder.and(builder.equal(root.get("userInfo"), oldUser), builder.equal(root.get("provider"), provider))).getSingle();
		credential.setUserInfo(newUser);
		databaseRepository.getCredentialDao().createOrUpdate(credential);
		List<UserDMP> userDmps = databaseRepository.getUserDmpDao().asQueryable().where((builder, root) -> builder.equal(root.get("user"), oldUser)).toList();
		userDmps.forEach(userDmp -> {
			userDmp.setUser(newUser);
			databaseRepository.getUserDmpDao().createOrUpdate(userDmp);
		});
		oldUser.setUserStatus((short)1);
		oldUser.setEmail(null);
		List<Credential> credentials = databaseRepository.getCredentialDao().asQueryable().where((builder, root) -> builder.equal(root.get("userInfo"), oldUser)).toList();
		credentials.forEach(cred -> {
			if (cred.getId() != credential.getId()) {
				databaseRepository.getCredentialDao().delete(cred);
			}
		});
		databaseRepository.getUserInfoDao().createOrUpdate(oldUser);
	}

	private void expireUserToken(UserInfo user) {
		UserToken userToken = databaseRepository.getUserTokenDao().asQueryable()
				.where((builder, root) -> builder.equal(root.get("user"), user))
				.orderBy((builder, root) -> builder.desc(root.get("issuedAt")))
				.take(1).toList().get(0);
		userToken.setExpiresAt(new Date());
		databaseRepository.getUserTokenDao().createOrUpdate(userToken);
	}
}
