package org.molgenis.ui.admin.usermanager;

import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import org.molgenis.auth.*;
import org.molgenis.data.DataService;
import org.molgenis.data.Query;
import org.molgenis.data.support.QueryImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.toList;
import static org.molgenis.auth.GroupMemberMetaData.GROUP_MEMBER;
import static org.molgenis.auth.GroupMetaData.GROUP;
import static org.molgenis.auth.UserMetaData.USER;

/**
 * Manage user in groups
 */
@Service
public class UserManagerServiceImpl implements UserManagerService
{
	private final DataService dataService;
	private final GroupMemberFactory groupMemberFactory;

	@Autowired
	public UserManagerServiceImpl(DataService dataService, GroupMemberFactory groupMemberFactory)
	{
		this.dataService = requireNonNull(dataService);
		this.groupMemberFactory = requireNonNull(groupMemberFactory);
	}

	@Override
	@PreAuthorize("hasAnyRole('ROLE_SU')")
	@Transactional(readOnly = true)
	public List<UserViewData> getAllUsers()
	{
		Stream<User> users = dataService.findAll(USER, User.class);
		return this.parseToMolgenisUserViewData(users);
	}

	@Override
	@PreAuthorize("hasAnyRole('ROLE_SU')")
	@Transactional
	public void setActivationUser(String userId, Boolean active)
	{
		User mu = this.dataService.findOneById(USER, userId, User.class);
		mu.setActive(active);
		this.dataService.update(USER, mu);
	}

	@Override
	@PreAuthorize("hasAnyRole('ROLE_SU')")
	@Transactional
	public void setActivationGroup(String groupId, Boolean active)
	{
		Group mg = this.dataService.findOneById(GROUP, groupId, Group.class);
		mg.setActive(active);
		this.dataService.update(GROUP, mg);
	}

	@Override
	@PreAuthorize("hasAnyRole('ROLE_SU')")
	@Transactional(readOnly = true)
	public List<Group> getAllGroups()
	{
		return dataService.findAll(GROUP, Group.class).collect(toList());
	}

	@Override
	@PreAuthorize("hasAnyRole('ROLE_SU')")
	@Transactional(readOnly = true)
	public List<Group> getGroupsWhereUserIsMember(String userId)
	{
		return this.getMolgenisGroups(userId);
	}

	@Override
	@PreAuthorize("hasAnyRole('ROLE_SU')")
	@Transactional(readOnly = true)
	public List<UserViewData> getUsersMemberInGroup(String groupId)
	{
		return this.parseToMolgenisUserViewData(this.getMolgenisUsers(groupId).stream());
	}

	private List<Group> getMolgenisGroups(String userId)
	{
		final User user = dataService.findOneById(USER, userId, User.class);

		if (user == null)
		{
			throw new RuntimeException("unknown user id [" + userId + "]");
		}

		final List<GroupMember> groupMembers = dataService
				.findAll(GROUP_MEMBER, new QueryImpl<GroupMember>().eq(GroupMemberMetaData.USER, user),
						GroupMember.class).collect(toList());

		return this.getAllMolgenisGroupsFromGroupMembers(groupMembers);
	}

	private List<User> getMolgenisUsers(final String groupId)
	{
		final Group group = dataService.findOneById(GROUP, groupId, Group.class);

		if (group == null)
		{
			throw new RuntimeException("unknown user id [" + groupId + "]");
		}

		final List<GroupMember> groupMembers = dataService
				.findAll(GROUP_MEMBER, new QueryImpl<GroupMember>().eq(GroupMemberMetaData.GROUP, group),
						GroupMember.class).collect(toList());

		return this.getAllMolgenisUsersFromGroupMembers(groupMembers);
	}

	@Override
	@PreAuthorize("hasAnyRole('ROLE_SU')")
	@Transactional(readOnly = true)
	public List<Group> getGroupsWhereUserIsNotMember(final String userId)
	{
		final User user = dataService.findOneById(USER, userId, User.class);

		if (user == null)
		{
			throw new RuntimeException("unknown user id [" + userId + "]");
		}

		final List<GroupMember> groupMembers = dataService
				.findAll(GROUP_MEMBER, new QueryImpl<GroupMember>().eq(GroupMemberMetaData.USER, user),
						GroupMember.class).collect(toList());

		final List<Group> groupsWhereUserIsMember = this.getAllMolgenisGroupsFromGroupMembers(groupMembers);

		Predicate<Group> predicate = new PredicateNotInMolgenisGroupList(groupsWhereUserIsMember);
		List<Group> groups = this.getAllGroups();

		return Lists.<Group>newArrayList(Iterables.filter(groups, predicate));
	}

	@Override
	@PreAuthorize("hasAnyRole('ROLE_SU')")
	@Transactional
	public void addUserToGroup(String molgenisGroupId, String molgenisUserId)
	{
		Group group = dataService.findOneById(GROUP, molgenisGroupId, Group.class);
		User user = dataService.findOneById(USER, molgenisUserId, User.class);

		GroupMember groupMember = groupMemberFactory.create();
		groupMember.setGroup(group);
		groupMember.setUser(user);
		dataService.add(GROUP_MEMBER, groupMember);
	}

	@Override
	@PreAuthorize("hasAnyRole('ROLE_SU')")
	@Transactional
	public void removeUserFromGroup(String molgenisGroupId, String molgenisUserId)
	{
		final User user = dataService.findOneById(USER, molgenisUserId, User.class);

		if (user == null)
		{
			throw new RuntimeException("unknown user id [" + molgenisUserId + "]");
		}

		final Group group = dataService.findOneById(GROUP, molgenisGroupId, Group.class);

		if (group == null)
		{
			throw new RuntimeException("unknown user id [" + molgenisGroupId + "]");
		}

		Query<GroupMember> q = new QueryImpl<GroupMember>().eq(GroupMemberMetaData.USER, user).and()
				.eq(GroupMemberMetaData.GROUP, group);

		final List<GroupMember> groupMembers = dataService.findAll(GROUP_MEMBER, q, GroupMember.class)
				.collect(toList());

		if (null == groupMembers || groupMembers.isEmpty())
		{
			throw new RuntimeException("molgenis group member is not found");
		}

		if (groupMembers.size() > 1)
		{
			throw new RuntimeException("there are more than one group member found");
		}

		GroupMember groupMember = groupMembers.get(0);
		dataService.delete(GROUP_MEMBER, groupMember);
	}

	/**
	 * Get All the molgenis groups from the list of molgenis group members
	 *
	 * @param groupMembers A list of MolgenisGroupMember instances
	 * @return List<MolgenisGroup>
	 */
	private List<Group> getAllMolgenisGroupsFromGroupMembers(final List<GroupMember> groupMembers)
	{
		List<Group> groups = new ArrayList<Group>();

		if (groupMembers != null && !groupMembers.isEmpty())
		{
			groups = Lists.transform(groupMembers, new Function<GroupMember, Group>()
			{
				@Override
				public Group apply(GroupMember groupMember)
				{
					return groupMember.getGroup();
				}
			});
		}

		return groups;
	}

	/**
	 * Get All the molgenis users from the list of molgenis group members
	 *
	 * @param groupMembers A list of MolgenisGroupMember instances
	 * @return List<MolgenisUser>
	 */
	private List<User> getAllMolgenisUsersFromGroupMembers(final List<GroupMember> groupMembers)
	{
		List<User> user = new ArrayList<User>();

		if (groupMembers != null && !groupMembers.isEmpty())
		{
			user = Lists.transform(groupMembers, new Function<GroupMember, User>()
			{
				@Override
				public User apply(GroupMember groupMember)
				{
					return groupMember.getUser();
				}
			});
		}

		return user;
	}

	private static class PredicateNotInMolgenisGroupList implements Predicate<Group>
	{
		final List<Group> toFilterItemList;

		PredicateNotInMolgenisGroupList(List<Group> notInList)
		{
			this.toFilterItemList = notInList;
		}

		@Override
		public boolean apply(Group item)
		{
			Object id = item.getId();
			for (Group toFilterItem : toFilterItemList)
			{
				if (toFilterItem.getId().equals(id)) return false;
			}
			return true;
		}

	}

	private List<UserViewData> parseToMolgenisUserViewData(Stream<User> users)
	{
		return users.map(user -> new UserViewData(user, getMolgenisGroups(user.getId()))).collect(toList());
	}
}
