/* ===================================================================
 * UserBizImpl.java
 * 
 * Created Sep 29, 2004 11:09:10 AM
 * 
 * Copyright (c) 2004 Matt Magoffin (spamsqr@msqr.us)
 * 
 * This program is free software; you can redistribute it and/or 
 * modify it under the terms of the GNU General Public License as 
 * published by the Free Software Foundation; either version 2 of 
 * the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of 
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License 
 * along with this program; if not, write to the Free Software 
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 
 * 02111-1307 USA
 * ===================================================================
 * $Id: UserBizImpl.java 23 2009-05-03 22:59:25Z msqr $
 * ===================================================================
 */

package magoffin.matt.ieat.biz.impl;

import java.util.Calendar;
import java.util.List;
import java.util.Locale;

import org.apache.log4j.Logger;
import org.springframework.beans.BeanUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import magoffin.matt.ieat.AuthorizationException;
import magoffin.matt.ieat.EatValidationException;
import magoffin.matt.ieat.biz.BizContext;
import magoffin.matt.ieat.biz.UserBiz;
import magoffin.matt.ieat.dao.UserDao;
import magoffin.matt.ieat.domain.User;
import magoffin.matt.util.DataEncryption;
import magoffin.matt.util.MessageDigester;

/**
 * Default implementation of UserBiz.
 * 
 * @author Matt Magoffin (spamsqr@msqr.us)
 * @version $Revision: 23 $ $Date: 2009-05-04 10:59:25 +1200 (Mon, 04 May 2009) $
 */
public class UserBizImpl implements UserBiz {
	
	private static final Logger LOG = Logger.getLogger(UserBizImpl.class);
	
	/* The following are externally injected fields */
	
	private byte[] salt = new byte[0];
	private UserDao userDao = null;
	private DataEncryption encryptor = null;
	private Validator userValidator = null;
	
	/**
	 * Initialize after properties set.
	 */
	public void init() {
		if ( userDao == null ) {
			throw new RuntimeException("userDao not configured");
		}
		if ( encryptor == null ) {
			throw new RuntimeException("encryptor not configured");
		}
		if ( userValidator == null ) {
			throw new RuntimeException("userValidator not configured");
		}
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.UserBiz#confirmRegisteredUser(java.lang.String, java.lang.String)
	 */
	public User confirmRegisteredUser(String login, String confirmationCode, BizContext context)
	throws AuthorizationException {
		User user = userDao.getUser(login);
		if ( user == null ) {
			throw new AuthorizationException(login,AuthorizationException.UNKNOWN_LOGIN);
		}
		
		if ( user.getAccessLevel() == null || user.getAccessLevel().intValue() > 0 ) {
			throw new AuthorizationException(login,AuthorizationException.REGISTRATION_ALREADY_CONFIRMED);
		}
		
		// validate confirmation string against user access number
		String decryptedConfCode = encryptor.decrypt(confirmationCode);
		
		// MySQL does not store milliseconds in the DateTime field, so we only compare up to the 
		// second here
		String confCode = String.valueOf(user.getCreatedDate().getTimeInMillis());
		confCode = user.getAccessLevel().toString() + confCode.substring(0,confCode.length()-3);	
		if ( decryptedConfCode == null || !decryptedConfCode.startsWith(confCode) ) {
			throw new AuthorizationException(login,AuthorizationException.REGISTRATION_NOT_CONFIRMED);
		}
		
		// adjust access level to null (ordinary user)
		user.setAccessLevel(null);
		
		// store user
		return userDao.get(userDao.store(user));
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.UserBiz#registerUser(magoffin.matt.ieat.domain.User, magoffin.matt.ieat.biz.BizContext)
	 */
	public String registerUser(User user, BizContext context) throws AuthorizationException {
		Errors errors = new BindException(user,"user");
		userValidator.validate(user,errors);
		if ( errors.hasErrors() ) {
			throw new EatValidationException(errors);
		}
		
		// generate random access level (negative), to use as confirmation
		Integer rnd = new Integer(-(int)(Math.random()*1000000000));
		user.setAccessLevel(rnd);
		
		// store user
		prepareUserForStorage(user);
		userDao.store(user);
		
		// generate confirmation string
		String conf = encryptor.encrypt(rnd.toString()+user.getCreatedDate().getTimeInMillis());
		if ( LOG.isDebugEnabled() ) {
			LOG.debug("Registered user '" +user.getLogin() +"' with confirmation '" +conf +"'");
		}
		return conf;
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.UserBiz#removeUser(java.lang.Integer, magoffin.matt.ieat.biz.BizContext)
	 */
	public void removeUser(Integer userId, BizContext context) {
		userDao.delete(userDao.get(userId));
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.UserBiz#getUsersWithAccess(int)
	 */
	public List<User> getUsersWithAccess(int level) {
		return userDao.getUsersForAccessLevel(level);
	}
	
	private void prepareUserForStorage(User user) throws AuthorizationException {
		// check for "unchanged" password value
		if ( user.getUserId() != null && DO_NOT_CHANGE_VALUE.equals(user.getPassword()) ) {
			// retrieve user from back-end and copy that password onto our user
			User realUser = userDao.get(user.getUserId());
			user.setPassword(realUser.getPassword());
		}
		
		// check password is encrypted
		if ( user.getPassword() != null && !(user.getPassword().startsWith("{SHA}") 
				|| user.getPassword().startsWith("{SSHA}")) ) {
			// encrypt the password now
			String encryptedPass = MessageDigester.generateDigest(user.getPassword(),salt);
			user.setPassword(encryptedPass);
		}
		if ( user.getCreatedDate() == null ) {
			user.setCreatedDate(Calendar.getInstance());
		}
		if ( user.getUserId() != null ) {
			User existingUser = userDao.getUser(user.getLogin());
			if ( existingUser != null && !user.getUserId().equals(existingUser.getUserId()) ) {
				throw new AuthorizationException(user.getLogin(),AuthorizationException.DUPLICATE_LOGIN);
			}
			existingUser = userDao.getUserByEmail(user.getEmail());
			if ( existingUser != null && !user.getUserId().equals(existingUser.getUserId()) ) {
				throw new AuthorizationException(user.getLogin(),AuthorizationException.DUPLICATE_EMAIL);
			}
		} else {
			User existingUser = userDao.getUser(user.getLogin());
			if ( existingUser != null ) {
				throw new AuthorizationException(user.getLogin(),AuthorizationException.DUPLICATE_LOGIN);
			}
			existingUser = userDao.getUserByEmail(user.getEmail());
			if ( existingUser != null ) {
				throw new AuthorizationException(user.getLogin(),AuthorizationException.DUPLICATE_EMAIL);
			}
		}
		// check country/lang set
		if ( user.getCountry() == null || user.getLanguage() == null ) {
			Locale l = Locale.getDefault();
			user.setCountry(l.getCountry());
			user.setLanguage(l.getLanguage());
		}
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.UserBiz#storeUser(magoffin.matt.ieat.domain.User, magoffin.matt.ieat.biz.BizContext)
	 */
	public User storeUser(User user, BizContext context) throws AuthorizationException {
		prepareUserForStorage(user);
		if ( user.getUserId() != null ) {
			User persistedUser = userDao.get(user.getUserId());
			if ( user != persistedUser ) {
				// copy properties into persistedUser
				BeanUtils.copyProperties(user, persistedUser);
				user = persistedUser;
			}
		}
		return userDao.get(userDao.store(user));
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.UserBiz#logonUser(java.lang.String, java.lang.String)
	 */
	public User logonUser(String login, String password)
	throws AuthorizationException {
		
		String attemptedPass = MessageDigester.generateDigest(password,salt);
		if ( LOG.isInfoEnabled() ) {
			LOG.info("Attempting to log in '" +login +"' with '" +attemptedPass +"'");
		}

		// get user from backend
		User user = userDao.getUser(login);
		
		if ( user == null ) {
			throw new AuthorizationException(login,AuthorizationException.UNKNOWN_LOGIN);
		}
		
		// verify not pending registration confirmation
		if ( user.getAccessLevel() != null && user.getAccessLevel().intValue() < 0 ) {
			throw new AuthorizationException(login,AuthorizationException.REGISTRATION_NOT_CONFIRMED);
		}
		
		// see if encrypted passwords match
		if ( !attemptedPass.equals(user.getPassword()) ) {
			throw new AuthorizationException(login,AuthorizationException.BAD_PASSWORD);
		}
		
		// update last login time
		Calendar now = Calendar.getInstance();
		user.setLastLoginDate(now);
		return userDao.get(userDao.store(user));
		
	}
	
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.UserBiz#hasAccessLevel(magoffin.matt.ieat.domain.User, int)
	 */
	public boolean hasAccessLevel(User user, int level) {
		return user != null && user.getAccessLevel() != null && 
			((user.getAccessLevel().intValue() & level) == 1);
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.UserBiz#getUserById(java.lang.Integer)
	 */
	public User getUserById(Integer userId, BizContext context) {
		return userDao.get(userId);
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.UserBiz#forgotPassword(java.lang.String, magoffin.matt.ieat.biz.BizContext)
	 */
	public String forgotPassword(String login, BizContext context) throws AuthorizationException {
		User user = userDao.getUser(login);
		if ( user == null ) {
			throw new AuthorizationException(login,AuthorizationException.UNKNOWN_LOGIN);
		}
		
		// change their password to something random
		String rnd = String.valueOf((long)(Math.random()*100000000000L));
		user.setPassword(rnd);
		
		prepareUserForStorage(user);
		userDao.store(user);
		
		// generate confirmation string
		String data = user.getCreatedDate().get(Calendar.DAY_OF_YEAR)+rnd;
		String conf = encryptor.encrypt(data);
		if ( LOG.isDebugEnabled() ) {
			LOG.debug("Generated user '" +user.getLogin() 
					+"' forgotten password confirmation with confirmation '" +conf +"'");
		}
		return conf;
	}
	
	/* (non-Javadoc)
	 * @see magoffin.matt.ieat.biz.UserBiz#confirmForgotPassword(java.lang.String, java.lang.String, java.lang.String, magoffin.matt.ieat.biz.BizContext)
	 */
	public User confirmForgotPassword(String login, String confirmationCode, String newPassword, BizContext context) 
	throws AuthorizationException {
		User user = userDao.getUser(login);
		if ( user == null ) {
			throw new AuthorizationException(login,AuthorizationException.UNKNOWN_LOGIN);
		}

		// validate confirmation string against user access number
		String decryptedConfCode = null;
		try {
			decryptedConfCode = encryptor.decrypt(confirmationCode);
		} catch ( RuntimeException e ) {
			if ( LOG.isDebugEnabled() ) {
				LOG.debug("Exception decrypting confirmation code: " +e.toString());
			}
			throw new AuthorizationException(login,AuthorizationException.FORGOTTEN_PASSWORD_NOT_CONFIRMED);
		}
		
		String dayOfYear = String.valueOf(user.getCreatedDate().get(Calendar.DAY_OF_YEAR));
		String unencryptedPassword = decryptedConfCode.substring(dayOfYear.length());
		String encryptedPass = MessageDigester.generateDigest(unencryptedPassword,salt);
		
		if ( encryptedPass == null || !encryptedPass.equals(user.getPassword()) ) {
			throw new AuthorizationException(login,AuthorizationException.FORGOTTEN_PASSWORD_NOT_CONFIRMED);
		}
		
		// ok, reset user password to new password
		user.setPassword(newPassword);
		
		// in case they have not actually registered before, reset their access level
		if ( user.getAccessLevel() != null && user.getAccessLevel().intValue() < 0 ) {
			user.setAccessLevel(null);
		}
		
		// store user
		prepareUserForStorage(user);
		return userDao.get(userDao.store(user));
	}

	/**
	 * @return the encryptor
	 */
	public DataEncryption getEncryptor() {
		return encryptor;
	}
	
	/**
	 * @param encryptor the encryptor to set
	 */
	public void setEncryptor(DataEncryption encryptor) {
		this.encryptor = encryptor;
	}
	
	/**
	 * @return the salt
	 */
	public byte[] getSalt() {
		return salt;
	}
	
	/**
	 * @param salt the salt to set
	 */
	public void setSalt(byte[] salt) {
		this.salt = salt;
	}
	
	/**
	 * @return the userDao
	 */
	public UserDao getUserDao() {
		return userDao;
	}
	
	/**
	 * @param userDao the userDao to set
	 */
	public void setUserDao(UserDao userDao) {
		this.userDao = userDao;
	}
	
	/**
	 * @return the userValidator
	 */
	public Validator getUserValidator() {
		return userValidator;
	}
	
	/**
	 * @param userValidator the userValidator to set
	 */
	public void setUserValidator(Validator userValidator) {
		this.userValidator = userValidator;
	}
	
}
