package org.jivesoftware.openfire.plugin.rest.controller;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.ws.rs.core.Response;

import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.SharedGroupException;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.group.GroupManager;
import org.jivesoftware.openfire.group.GroupNotFoundException;
import org.jivesoftware.openfire.lockout.LockOutManager;
import org.jivesoftware.openfire.plugin.rest.dao.PropertyDAO;
import org.jivesoftware.openfire.plugin.rest.entity.GroupEntity;
import org.jivesoftware.openfire.plugin.rest.entity.RosterEntities;
import org.jivesoftware.openfire.plugin.rest.entity.RosterItemEntity;
import org.jivesoftware.openfire.plugin.rest.entity.UserEntities;
import org.jivesoftware.openfire.plugin.rest.entity.UserEntity;
import org.jivesoftware.openfire.plugin.rest.entity.UserGroupsEntity;
import org.jivesoftware.openfire.plugin.rest.entity.UserProperty;
import org.jivesoftware.openfire.plugin.rest.exceptions.ExceptionType;
import org.jivesoftware.openfire.plugin.rest.exceptions.ServiceException;
import org.jivesoftware.openfire.plugin.rest.utils.UserUtils;
import org.jivesoftware.openfire.roster.Roster;
import org.jivesoftware.openfire.roster.RosterItem;
import org.jivesoftware.openfire.roster.RosterManager;
import org.jivesoftware.openfire.session.ClientSession;
import org.jivesoftware.openfire.user.User;
import org.jivesoftware.openfire.user.UserAlreadyExistsException;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.xmpp.packet.JID;
import org.xmpp.packet.StreamError;

/**
 * The Class UserServiceController.
 */
public class UserServiceController {
	/** The Constant INSTANCE. */
	public static final UserServiceController INSTANCE = new UserServiceController();

	/** The user manager. */
	private UserManager userManager;

	/** The roster manager. */
	private RosterManager rosterManager;

	/** The server. */
	private XMPPServer server;
	
	/** The lock out manager. */
	private LockOutManager lockOutManager;

	/**
	 * Gets the single instance of UserServiceController.
	 *
	 * @return single instance of UserServiceController
	 */
	public static UserServiceController getInstance() {
		return INSTANCE;
	}

	/**
	 * Instantiates a new user service controller.
	 */
	private UserServiceController() {
		server = XMPPServer.getInstance();
		userManager = server.getUserManager();
		rosterManager = server.getRosterManager();
		lockOutManager = server.getLockOutManager();
	}

	/**
	 * Creates the user.
	 *
	 * @param userEntity
	 *            the user entity
	 * @throws ServiceException
	 *             the service exception
	 */
	public void createUser(UserEntity userEntity) throws ServiceException {
		if (userEntity != null && !userEntity.getUsername().isEmpty()) {
			if (userEntity.getPassword() == null) {
				throw new ServiceException("Could not create new user, because password is null",
						userEntity.getUsername(), "PasswordIsNull", Response.Status.BAD_REQUEST);
			}
			try {
				userManager.createUser(userEntity.getUsername(), userEntity.getPassword(), userEntity.getName(),
						userEntity.getEmail());
			} catch (UserAlreadyExistsException e) {
				throw new ServiceException("Could not create new user", userEntity.getUsername(),
						ExceptionType.USER_ALREADY_EXISTS_EXCEPTION, Response.Status.CONFLICT);
			}
			addProperties(userEntity.getUsername(), userEntity.getProperties());
		} else {
			throw new ServiceException("Could not create new user",
					"users", ExceptionType.ILLEGAL_ARGUMENT_EXCEPTION, Response.Status.BAD_REQUEST);
		}
	}

	/**
	 * Update user.
	 *
	 * @param username
	 *            the username
	 * @param userEntity
	 *            the user entity
	 * @throws ServiceException
	 *             the service exception
	 */
	public void updateUser(String username, UserEntity userEntity) throws ServiceException {
		if (userEntity != null && !username.isEmpty()) {
			// Payload contains another username than provided over path
			// parameter
			if (userEntity.getUsername() != null) {
				if (!userEntity.getUsername().equals(username)) {
					JustMarriedController.changeName(username, userEntity.getUsername(), true, userEntity.getEmail(),
							userEntity.getName());
					addProperties(userEntity.getUsername(), userEntity.getProperties());
					return;
				}
			}
			User user = getAndCheckUser(username);
			if (userEntity.getPassword() != null) {
				user.setPassword(userEntity.getPassword());
			}
			if (userEntity.getName() != null) {
				user.setName(userEntity.getName());
			}
			if (userEntity.getEmail() != null) {
				user.setEmail(userEntity.getEmail());
			}

			addProperties(username, userEntity.getProperties());
		}
	}

	/**
	 * Delete user.
	 *
	 * @param username
	 *            the username
	 * @throws ServiceException
	 *             the service exception
	 */
	public void deleteUser(String username) throws ServiceException {
		User user = getAndCheckUser(username);
		userManager.deleteUser(user);

		rosterManager.deleteRoster(server.createJID(username, null));
	}

	/**
	 * Gets the user entities.
	 *
	 * @param userSearch
	 *            the user search
	 * @param propertyValue
	 * @param propertyKey
	 * @return the user entities
	 * @throws ServiceException
	 */
	public UserEntities getUserEntities(String userSearch, String propertyKey, String propertyValue)
			throws ServiceException {
		if (propertyKey != null) {
			return getUserEntitiesByProperty(propertyKey, propertyValue);
		}
		UserEntities userEntities = new UserEntities();
		userEntities.setUsers(UserUtils.convertUsersToUserEntities(userManager.getUsers(), userSearch));
		return userEntities;
	}

	/**
	 * Gets the user entity.
	 *
	 * @param username
	 *            the username
	 * @return the user entity
	 * @throws ServiceException
	 *             the service exception
	 */
	public UserEntity getUserEntity(String username) throws ServiceException {
		return UserUtils.convertUserToUserEntity(getAndCheckUser(username));
	}

	/**
	 * Enable user.
	 *
	 * @param username
	 *            the username
	 * @throws ServiceException
	 *             the service exception
	 */
	public void enableUser(String username) throws ServiceException {
		getAndCheckUser(username);
		lockOutManager.enableAccount(username);
	}

	/**
	 * Disable user.
	 *
	 * @param username
	 *            the username
	 * @throws ServiceException
	 *             the service exception
	 */
	public void disableUser(String username) throws ServiceException {
		getAndCheckUser(username);
		lockOutManager.disableAccount(username, null, null);
		
        if (lockOutManager.isAccountDisabled(username)) {
            final StreamError error = new StreamError(StreamError.Condition.not_authorized);
            for (ClientSession sess : SessionManager.getInstance().getSessions(username)) {
                sess.deliverRawText(error.toXML());
                sess.close();
            }
        }
	}

	/**
	 * Gets the roster entities.
	 *
	 * @param username
	 *            the username
	 * @return the roster entities
	 * @throws ServiceException
	 *             the service exception
	 */
	public RosterEntities getRosterEntities(String username) throws ServiceException {
		Roster roster = getUserRoster(username);

		List<RosterItemEntity> rosterEntities = new ArrayList<RosterItemEntity>();
		for (RosterItem rosterItem : roster.getRosterItems()) {
			RosterItemEntity rosterItemEntity = new RosterItemEntity(rosterItem.getJid().toBareJID(),
					rosterItem.getNickname(), rosterItem.getSubStatus().getValue());
			rosterItemEntity.setGroups(rosterItem.getGroups());

			rosterEntities.add(rosterItemEntity);
		}

		return new RosterEntities(rosterEntities);
	}

	/**
	 * Adds the roster item.
	 *
	 * @param username
	 *            the username
	 * @param rosterItemEntity
	 *            the roster item entity
	 * @throws ServiceException
	 *             the service exception
	 * @throws UserAlreadyExistsException
	 *             the user already exists exception
	 * @throws SharedGroupException
	 *             the shared group exception
	 * @throws UserNotFoundException
	 *             the user not found exception
	 */
	public void addRosterItem(String username, RosterItemEntity rosterItemEntity) throws ServiceException,
			UserAlreadyExistsException, SharedGroupException, UserNotFoundException {
		Roster roster = getUserRoster(username);
		if (rosterItemEntity.getJid() == null) {
			throw new ServiceException("JID is null", "JID", "IllegalArgumentException", Response.Status.BAD_REQUEST);
		}
		JID jid = new JID(rosterItemEntity.getJid());

		try {
			roster.getRosterItem(jid);
			throw new UserAlreadyExistsException(jid.toBareJID());
		} catch (UserNotFoundException e) {
			// Roster item does not exist. Try to add it.
		}

		if (roster != null) {
			RosterItem rosterItem = roster.createRosterItem(jid, rosterItemEntity.getNickname(),
					rosterItemEntity.getGroups(), false, true);
			UserUtils.checkSubType(rosterItemEntity.getSubscriptionType());
			rosterItem.setSubStatus(RosterItem.SubType.getTypeFromInt(rosterItemEntity.getSubscriptionType()));
			roster.updateRosterItem(rosterItem);
		}
	}

	/**
	 * Update roster item.
	 *
	 * @param username
	 *            the username
	 * @param rosterJid
	 *            the roster jid
	 * @param rosterItemEntity
	 *            the roster item entity
	 * @throws ServiceException
	 *             the service exception
	 * @throws UserNotFoundException
	 *             the user not found exception
	 * @throws UserAlreadyExistsException
	 *             the user already exists exception
	 * @throws SharedGroupException
	 *             the shared group exception
	 */
	public void updateRosterItem(String username, String rosterJid, RosterItemEntity rosterItemEntity)
			throws ServiceException, UserNotFoundException, UserAlreadyExistsException, SharedGroupException {
		getAndCheckUser(username);

		Roster roster = getUserRoster(username);
		JID jid = new JID(rosterJid);
		RosterItem rosterItem = roster.getRosterItem(jid);

		if (rosterItemEntity.getNickname() != null) {
			rosterItem.setNickname(rosterItemEntity.getNickname());
		}
		if (rosterItemEntity.getGroups() != null) {
			rosterItem.setGroups(rosterItemEntity.getGroups());
		}
		UserUtils.checkSubType(rosterItemEntity.getSubscriptionType());

		rosterItem.setSubStatus(RosterItem.SubType.getTypeFromInt(rosterItemEntity.getSubscriptionType()));
		roster.updateRosterItem(rosterItem);
	}

	/**
	 * Delete roster item.
	 *
	 * @param username
	 *            the username
	 * @param rosterJid
	 *            the roster jid
	 * @throws SharedGroupException
	 *             the shared group exception
	 * @throws ServiceException
	 *             the service exception
	 */
	public void deleteRosterItem(String username, String rosterJid) throws SharedGroupException, ServiceException {
		getAndCheckUser(username);
		Roster roster = getUserRoster(username);
		JID jid = new JID(rosterJid);

		if (roster.deleteRosterItem(jid, true) == null) {
			throw new ServiceException("Roster Item could not deleted", jid.toBareJID(), "RosterItemNotFound",
					Response.Status.NOT_FOUND);
		}
	}

	/**
	 * Gets the user groups.
	 *
	 * @param username
	 *            the username
	 * @return the user groups
	 * @throws ServiceException
	 *             the service exception
	 */
	public List<String> getUserGroups(String username) throws ServiceException {
		User user = getAndCheckUser(username);
		Collection<Group> groups = GroupManager.getInstance().getGroups(user);
		List<String> groupNames = new ArrayList<String>();
		for (Group group : groups) {
			groupNames.add(group.getName());
		}

		return groupNames;
	}

	/**
	 * Adds the user to group.
	 *
	 * @param username
	 *            the username
	 * @param userGroupsEntity
	 *            the user groups entity
	 * @throws ServiceException
	 *             the service exception
	 */
	public void addUserToGroups(String username, UserGroupsEntity userGroupsEntity) throws ServiceException {
		if (userGroupsEntity != null) {
			Collection<Group> groups = new ArrayList<Group>();

			for (String groupName : userGroupsEntity.getGroupNames()) {
				Group group = null;
				try {
					group = GroupManager.getInstance().getGroup(groupName);
				} catch (GroupNotFoundException e) {
					// Create this group
					group = GroupController.getInstance().createGroup(new GroupEntity(groupName, ""));
				}
				groups.add(group);
			}
			for (Group group : groups) {
				group.getMembers().add(server.createJID(username, null));
			}
		}
	}
	
	/**
	 * Adds the user to group.
	 *
	 * @param username the username
	 * @param groupName the group name
	 * @throws ServiceException the service exception
	 */
	public void addUserToGroup(String username, String groupName) throws ServiceException {
		Group group = null;
		try {
			group = GroupManager.getInstance().getGroup(groupName);
		} catch (GroupNotFoundException e) {
			// Create this group
			group = GroupController.getInstance().createGroup(new GroupEntity(groupName, ""));
		}
		
		group.getMembers().add(server.createJID(username, null));
	}

	/**
	 * Delete user from groups.
	 *
	 * @param username
	 *            the username
	 * @param userGroupsEntity
	 *            the user groups entity
	 * @throws ServiceException
	 *             the service exception
	 */
	public void deleteUserFromGroups(String username, UserGroupsEntity userGroupsEntity) throws ServiceException {
		if (userGroupsEntity != null) {
			for (String groupName : userGroupsEntity.getGroupNames()) {
				Group group = null;
				try {
					group = GroupManager.getInstance().getGroup(groupName);
				} catch (GroupNotFoundException e) {
					throw new ServiceException("Could not find group", groupName, ExceptionType.GROUP_NOT_FOUND,
							Response.Status.NOT_FOUND, e);
				}
				group.getMembers().remove(server.createJID(username, null));
			}
		}
	}
	
	/**
	 * Delete user from group.
	 *
	 * @param username the username
	 * @param groupName the group name
	 * @throws ServiceException the service exception
	 */
	public void deleteUserFromGroup(String username, String groupName) throws ServiceException {
		Group group = null;
		try {
			group = GroupManager.getInstance().getGroup(groupName);
		} catch (GroupNotFoundException e) {
			throw new ServiceException("Could not find group", groupName, ExceptionType.GROUP_NOT_FOUND,
					Response.Status.NOT_FOUND, e);
		}
		group.getMembers().remove(server.createJID(username, null));
	}

	/**
	 * Gets the user entities by property key and or value.
	 *
	 * @param propertyKey
	 *            the property key
	 * @param propertyValue
	 *            the property value (can be null)
	 * @return the user entities by property
	 * @throws ServiceException
	 *             the service exception
	 */
	public UserEntities getUserEntitiesByProperty(String propertyKey, String propertyValue) throws ServiceException {
		List<String> usernames = PropertyDAO.getUsernameByProperty(propertyKey, propertyValue);
		List<UserEntity> users = new ArrayList<UserEntity>();
		UserEntities userEntities = new UserEntities();

		for (String username : usernames) {
			users.add(getUserEntity(username));
		}

		userEntities.setUsers(users);
		return userEntities;
	}

	/**
	 * Adds the properties.
	 *
	 * @param userEntity
	 *            the user entity
	 * @throws ServiceException
	 *             the service exception
	 */
	private void addProperties(String username, List<UserProperty> properties) throws ServiceException {
		User user = getAndCheckUser(username);
		user.getProperties().clear();
		if (properties != null) {
			for (UserProperty property : properties) {
				user.getProperties().put(property.getKey(), property.getValue());
			}
		}
	}

	/**
	 * Gets the and check user.
	 *
	 * @param username
	 *            the username
	 * @return the and check user
	 * @throws ServiceException
	 *             the service exception
	 */
	private User getAndCheckUser(String username) throws ServiceException {
		JID targetJID = server.createJID(username, null);
		if (targetJID.getNode() == null) {
			throw new ServiceException("Could not get user", username, ExceptionType.USER_NOT_FOUND_EXCEPTION,
					Response.Status.NOT_FOUND);
		}

		try {
			return userManager.getUser(targetJID.getNode());
		} catch (UserNotFoundException e) {
			throw new ServiceException("Could not get user", username, ExceptionType.USER_NOT_FOUND_EXCEPTION,
					Response.Status.NOT_FOUND, e);
		}
	}

	/**
	 * Gets the user roster.
	 *
	 * @param username
	 *            the username
	 * @return the user roster
	 * @throws ServiceException
	 *             the service exception
	 */
	private Roster getUserRoster(String username) throws ServiceException {
		try {
			return rosterManager.getRoster(username);
		} catch (UserNotFoundException e) {
			throw new ServiceException("Could not get user roster", username, ExceptionType.USER_NOT_FOUND_EXCEPTION,
					Response.Status.NOT_FOUND, e);
		}
	}
}
