/*******************************************************************************
 * Copyright 2Fi Business Solutions Ltd.
 *
 * This code is copyrighted. Under no circumstances should any party, people,
 * or organization should redistribute any portions of this code in any form,
 * either verbatim or through electronic media, to any third parties, unless
 * under explicit written permission by 2Fi Business Solutions Ltd.
 ******************************************************************************/
package com.ffii.core.security.service;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.ffii.core.User;
import com.ffii.core.dao.JdbcDao;
import com.ffii.core.utils.MapUtils;
import com.ffii.core.web.AbstractService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;

/**
 * Token User Details Service
 *
 * @author Patrick
 */
@Service
public class TokenUserDetailsService extends AbstractService {

	@Autowired
	private JdbcDao jdbcDao;

	public static final String FIND_USER_BY_DEVICE_TOKEN_SQL = "SELECT u.* FROM users u LEFT JOIN access_token at ON u.id = at.userId"
			+ " WHERE u.deleted = 0 AND at.token = :token";

	public static final String LOAD_AUTHORITIES_BY_USERNAME_SQL = "SELECT u.username, ua.authority"
			+ " FROM `users_authorities` ua" + " LEFT JOIN `users` u ON ua.userId = u.id"
			+ " WHERE u.deleted = 0 AND u.username = ?";

	public static final String LOAD_GROUP_AUTHORITIES_BY_USERNAME_SQL = "SELECT g.id, g.name, ga.authority"
			+ " FROM `groups_authorities` ga" + " LEFT JOIN `groups` g ON ga.groupId = g.id AND g.deleted = 0"
			+ " LEFT JOIN `groups_users` gu ON ga.groupId = gu.groupId" + " LEFT JOIN `users` u ON gu.userId = u.id"
			+ " WHERE u.deleted = 0 AND u.username = ?";

	protected final MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

	// ~ Constructors
	public TokenUserDetailsService() {
	}

	// ~ Methods

	/**
	 * Allows subclasses to add their own granted authorities to the list to be
	 * returned in the <tt>UserDetails</tt>.
	 *
	 * @param username    the username, for use by finder methods
	 * @param authorities the current granted authorities, as populated from the
	 *                    <code>authoritiesByUsername</code> mapping
	 */
	protected void addCustomAuthorities(String username, List<GrantedAuthority> authorities) {
		// add ROLE_USER for basic access
		authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
	}

	public UserDetails loadUserByToken(String token) {

		User user = findUserByToken(token); // contains no GrantedAuthority[]

		// build GrantedAuthority[]
		Set<GrantedAuthority> authoritiesSet = new HashSet<>();

		// aadd all user's authorities
		authoritiesSet.addAll(loadUserAuthorities(user.getUsername()));

		// add all user's groups' authorities
		authoritiesSet.addAll(loadGroupAuthorities(user.getUsername()));

		// convert to List
		List<GrantedAuthority> authoritiesList = new ArrayList<GrantedAuthority>(authoritiesSet);

		addCustomAuthorities(user.getUsername(), authoritiesList);

		user.setAuthorities(authoritiesList);

		return user;
	}

	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	public User findUserByToken(String token) {
		List<User> users = jdbcDao.query(FIND_USER_BY_DEVICE_TOKEN_SQL, User.class, MapUtils.toHashMap("token", token),
				1);
		if (users.size() == 1)
			return users.get(0);
		else
			return null;
	}

	/**
	 * Loads user authorities by executing SQL
	 *
	 * @return a list of GrantedAuthority objects for the user
	 */
	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	protected List<GrantedAuthority> loadUserAuthorities(String username) {
		return jdbcDao.getNamedParameterJdbcTemplate().query(LOAD_AUTHORITIES_BY_USERNAME_SQL,
				MapUtils.toHashMap("username", username), new RowMapper<GrantedAuthority>() {
					@Override
					public GrantedAuthority mapRow(ResultSet rs, int rowNum) throws SQLException {
						return new SimpleGrantedAuthority(rs.getString("authority"));
					}
				});
	}

	/**
	 * Loads group authorities by executing SQL
	 *
	 * @return a list of GrantedAuthority objects from the user's groups
	 */
	@Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class, readOnly = true)
	protected List<GrantedAuthority> loadGroupAuthorities(String username) {
		return jdbcDao.getNamedParameterJdbcTemplate().query(LOAD_GROUP_AUTHORITIES_BY_USERNAME_SQL,
				MapUtils.toHashMap("username", username), new RowMapper<GrantedAuthority>() {
					@Override
					public GrantedAuthority mapRow(ResultSet rs, int rowNum) throws SQLException {
						return new SimpleGrantedAuthority(rs.getString("authority"));
					}
				});
	}

}
