package com.atlassian.confluence.rpc.soap.services;

import com.atlassian.confluence.rpc.NotPermittedException;
import com.atlassian.confluence.rpc.RemoteException;
import com.atlassian.confluence.rpc.soap.beans.RemoteUser;
import com.atlassian.confluence.rpc.soap.beans.RemoteUserInformation;
import com.atlassian.confluence.security.Permission;
import com.atlassian.confluence.security.PermissionManager;
import com.atlassian.confluence.security.SpacePermissionManager;
import com.atlassian.confluence.spaces.SpaceManager;
import com.atlassian.confluence.spaces.Space;
import com.atlassian.confluence.user.*;
import com.atlassian.confluence.util.GeneralUtil;
import com.atlassian.confluence.util.ProfilePicture;
import com.atlassian.confluence.pages.Attachment;
import com.atlassian.confluence.pages.AttachmentManager;
import com.atlassian.user.Group;
import com.atlassian.user.User;
import com.atlassian.user.EntityException;
import com.atlassian.user.search.page.Pager;
import com.atlassian.core.user.preferences.UserPreferences;
import com.atlassian.core.AtlassianCoreException;

import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.File;

public class UsersSoapService
{
    UserAccessor userAccessor;
    SpacePermissionManager spacePermissionManager;
    PermissionManager permissionManager;
    SoapServiceHelper soapServiceHelper;
    SpaceManager spaceManager;
    PersonalInformationManager personalInformationManager;
    AttachmentManager attachmentManager;

    private UserContentManager userContentManager;

    public void setUserContentManager(UserContentManager userContentManager)
    {
        this.userContentManager = userContentManager;
    }

    public void setAttachmentManager(AttachmentManager attachmentManager) {
        this.attachmentManager = attachmentManager;
    }

    public void setUserAccessor(UserAccessor userAccessor)
    {
        this.userAccessor = userAccessor;
    }

    public void setSpacePermissionManager(SpacePermissionManager spacePermissionManager)
    {
        this.spacePermissionManager = spacePermissionManager;
    }

    public void setSoapServiceHelper(SoapServiceHelper soapServiceHelper)
    {
        this.soapServiceHelper = soapServiceHelper;
    }

    public void setSpaceManager(SpaceManager spaceManager)
    {
        this.spaceManager = spaceManager;
    }

    public void setPermissionManager(PermissionManager permissionManager)
    {
        this.permissionManager = permissionManager;
    }

    private boolean canUserAdminister(User currentUser)
    {
        return (GeneralUtil.isSuperUser(currentUser) || permissionManager.hasPermission(currentUser, Permission.ADMINISTER, null));
    }

    public RemoteUser getUser(String username) throws RemoteException
    {
        return new RemoteUser(retrieveUser(username));
    }

    private User retrieveUser(String username)
            throws RemoteException
    {
        User user = userAccessor.getUser(username);
        if (user == null)
            throw new RemoteException("No user with that username found.");

        return user;
    }

    public boolean addUser(RemoteUser user, String password) throws RemoteException
    {
        soapServiceHelper.assertCanAdministrate();
        userAccessor.addUser(user.getName(), password, user.getEmail(), user.getFullname(), new String[]{UserAccessor.GROUP_CONFLUENCE_USERS});
        return true;
    }

    /**
     * Delete the given user from the Confluence System.
     *
     * @param username is the name of the user that is to be deleted from the CONFLUENCE SYSTEM.
     * @throws RemoteException if user with the given name does not exist or if the authenticated user does not have
     *                         ADMINISTRATE CONFLUENCE PERMISSION or if THE USER HAS AUTHORED CONTENT
     */

    public boolean removeUser(String username) throws RemoteException
    {
        soapServiceHelper.assertCanAdministrate();
        User user = this.retrieveUser(username);
        //if user has authored content then he cannot be deleted therefore, throw exception.
        if (userContentManager.hasAuthoredContent(user))
        {
            throw new RemoteException("cannot remove user because he has authored content");
        }
        Space personalSpace = spaceManager.getPersonalSpace(user);
        if (personalSpace != null)
            spaceManager.removeSpace(personalSpace);

        userAccessor.removeUser(user);
        return true;
    }

    public boolean editUser(RemoteUser remoteUser) throws NotPermittedException, RemoteException
    {
        User currentUser = AuthenticatedUserThreadLocal.getUser();
        User givenUser = retrieveUser(remoteUser.getName());
        boolean updateRequired = false;

        // If the current user can't administrate
        if (!permissionManager.hasPermission(currentUser, Permission.EDIT, givenUser))
            throw new NotPermittedException("You are not logged in as the correct user, or you do not have the correct permissions to perform this action.");

        // Compare the user attributes
        if (!givenUser.getName().equals(remoteUser.getName()))
            throw new RemoteException("Cannot change the username");

        // Full name
        if (!givenUser.getFullName().equals(remoteUser.getFullname()))
        {
            updateRequired = true;

            givenUser.setFullName(remoteUser.getFullname());
        }

        // Email
        if (!givenUser.getEmail().equals(remoteUser.getEmail()))
        {
            updateRequired = true;

            givenUser.setEmail(remoteUser.getEmail());
        }

        if (updateRequired)
            userAccessor.saveUser(givenUser);

        return true;
    }

    public String[] getUserGroups(String username) throws RemoteException
    {
        soapServiceHelper.assertCanAdministrate();
        User user = retrieveUser(username);
        Pager groups = userAccessor.getGroups(user);
        ArrayList groupsOfUser = new ArrayList();

        Iterator iter = groups.iterator();
        while (iter.hasNext())
        {
            Group group = (Group) iter.next();
            groupsOfUser.add(group.getName());
        }

        return (String[]) groupsOfUser.toArray(new String[1]);
    }

    public boolean addUserToGroup(String username, String groupname) throws RemoteException
    {
        soapServiceHelper.assertCanAdministrate();
        User user = retrieveUser(username);
        Group group = userAccessor.getGroup(groupname);

        if (group == null)
        {
            throw new RemoteException("The group specified does not exist.");
        }
        userAccessor.addMembership(group, user);
        return true;
    }

    /**
     * Delete the user with name username from the group with name groupname.
     *
     * @param username  is the name of the user that is to be deleted from the group with wih name groupname.
     * @param groupname is the name of the group from which the user with name username is to be deleted.
     * @throws RemoteException if user/group with the given names does not exist or if the authenticated user does not have
     *                         ADMINISTRATE CONFLUENCE PERMISSION
     */
    public boolean removeUserFromGroup(String username, String groupname) throws RemoteException
    {
        soapServiceHelper.assertCanAdministrate();
        User user = this.retrieveUser(username);
        Group group = userAccessor.getGroup(groupname);
        if (group == null)
        {
            throw new RemoteException("The group specified does not exist.");
        }

        userAccessor.removeMembership(group, user);
        return true;
    }


    public boolean addGroup(String groupname) throws RemoteException
    {
        soapServiceHelper.assertCanAdministrate();

        if (userAccessor.getGroup(groupname) == null)
        {
            userAccessor.addGroup(groupname);
            return true;
        }

        return false;
    }

    /**
     * Delete all permissions (global and space level) permissions for the group with the given name.
     *
     * @param groupname is the group for which all the permissions has to be deleted
     * @throws RemoteException if group with the given name does not exist or if the authenticated user does not have
     *                         ADMINISTRATE CONFLUENCE PERMISSION
     */
    public boolean removeAllPermissionsForGroup(String groupname) throws RemoteException
    {
        soapServiceHelper.assertCanAdministrate();
        Group group = userAccessor.getGroup(groupname);
        if (group == null)
        {
            throw new RemoteException("cannot delete permissions for non existing group");
        }
        spacePermissionManager.removeAllPermissionsForGroup(groupname);
        return true;
    }

    /**
     * Delete group with name groupname. If defaultGroupName is specified i.e not null and valid, then, add users
     * from the group with name groupname to the defaultgroup and deleteGroup
     *
     * @param groupName        is the group that is to be deleted
     * @param defaultGroupName is the default group to which the users from the group to be deleted will be added
     * @throws RemoteException if groups with givenname/defaultName does not exist or if  the authenticated user does not have
     *                         ADMINISTRATE CONFLUENCE PERMISSION
     */
    public boolean removeGroup(String groupName, String defaultGroupName) throws RemoteException
    {
        //Check if the authenticated user has ADMINISTRATE CONFLUENCE PERMISSION
        soapServiceHelper.assertCanAdministrate();

        //Retieve the group to be deleted
        Group group = userAccessor.getGroup(groupName);
        if (group == null)
        {
            throw new RemoteException("cannot remove non existing group");
        }

        //Check if user has specified a default group.
        boolean hasDefault = false;
        Group defaultGroup = null;
        if (defaultGroupName != null && defaultGroupName.length() != 0)
        {
            defaultGroup = userAccessor.getGroup(defaultGroupName);
            if (defaultGroup == null)
            {
                throw new RemoteException("default group does not exist");
            }
            hasDefault = true;
        }

        //If Default group is specified and present then add users to default before deleting group
        for (Iterator iterator = userAccessor.getMemberNames(group).iterator(); iterator.hasNext();)
        {
            String username = (String) iterator.next();
            if (hasDefault)
            {
                userAccessor.addMembership(defaultGroup.getName(), username);
            }
        }

        // Remove the group. The manager will take care of removing membership information if required.
        userAccessor.removeGroup(group);
        spacePermissionManager.removeAllPermissionsForGroup(group.getName());

        return true;
    }

    public String[] getGroups() throws RemoteException
    {
        soapServiceHelper.assertCanAdministrate();
        Pager groups = userAccessor.getGroups();

        ArrayList groupsOfUser = new ArrayList();

        Iterator iter = groups.iterator();

        int i = 0;
        while (iter.hasNext())
        {
            Group group = (Group) iter.next();
            groupsOfUser.add(group.getName());
        }

        return (String[]) groupsOfUser.toArray(new String[1]);
    }

    public boolean deactivateUser(String username) throws NotPermittedException, RemoteException
    {
        soapServiceHelper.assertCanAdministrate();

        User user = retrieveUser(username);

        // If the user isn't active
        if (userAccessor.isDeactivated(user))
            throw new RemoteException("User has already been deactivated");

        userAccessor.deactivateUser(user);

        return true;
    }

    public boolean reactivateUser(String username) throws NotPermittedException, RemoteException
    {
        soapServiceHelper.assertCanAdministrate();

        User user = retrieveUser(username);

        // If the user hasn't already been deactivated
        if (!userAccessor.isDeactivated(user))
            throw new RemoteException("User is already active");

        userAccessor.reactivateUser(user);

        return true;
    }

    public String[] getActiveUsers(boolean viewAll) throws RemoteException
    {
        soapServiceHelper.assertCanAdministrate();

        Pager tempPager = null;
        List userList = new ArrayList();

        // Grab whatever users we need
        if (viewAll)
            tempPager = userAccessor.getUsers();
        else
            tempPager = userAccessor.getUsersWithConfluenceAccess();

        // Iterate through and covert to Usernames
        Iterator iter = tempPager.iterator();

        while (iter.hasNext())
        {
            User user = (User) iter.next();
            userList.add(user.getName());
        }

        return (String[]) userList.toArray(new String[userList.size()]);
    }

    public boolean changeMyPassword(String oldPass, String newPass) throws NotPermittedException, RemoteException
    {
        if (newPass == null || newPass.length() == 0)
            throw new RemoteException("New password cannot be null or empty");

        User currentUser = AuthenticatedUserThreadLocal.getUser();

        if (!userAccessor.authenticate(currentUser.getName(), oldPass))
            throw new NotPermittedException("The current password was incorrect. Please try again.");

        // Change the password and save the User
        try
        {
            userAccessor.alterPassword(currentUser, newPass);
        }
        catch (EntityException e)
        {
            throw new RemoteException(e);
        }

        return true;
    }

    public boolean changeUserPassword(String username, String newPass) throws NotPermittedException, RemoteException
    {
        // Check that the new and confirmation passwords are legit, then check that they are the same
        if (newPass == null || newPass.length() == 0)
            throw new RemoteException("New password cannot be null or empty.");

        User currentUser = AuthenticatedUserThreadLocal.getUser();
        User givenUser = retrieveUser(username);

        // If the current user can't administrate
        if (!canUserAdminister(currentUser))
            throw new NotPermittedException("You do not have the correct permissions to perform this action.");
        try
        {
            // Change the password
            userAccessor.alterPassword(givenUser,newPass);
        }
        catch (EntityException ee)
        {
            throw new RemoteException("Error changing password for user " + username, ee);
        }

        return true;
    }

    public boolean setUserInformation(RemoteUserInformation userInfo) throws RemoteException
    {
        User currentUser = AuthenticatedUserThreadLocal.getUser();
        User givenUser = retrieveUser(userInfo.getUsername());

        // If the current user is not the given user, or they can't administrate
        if (!permissionManager.hasPermission(currentUser, Permission.EDIT, givenUser))
            throw new NotPermittedException("You are not logged in as the correct user, or you do not have the correct permissions to perform this action.");

        // Check values passed in
        if (userInfo.getContent() != null)
        {
            // We don't want to modify anything except the content, so copy the current personal information
            PersonalInformation newInfo = personalInformationManager.getPersonalInformation(givenUser.getName());
            PersonalInformation oldInfo = null;

            // If we didn't get anything back, create a new PersonalInformation instance for the user
            if (newInfo == null)
            {
                newInfo = new PersonalInformation(givenUser, userAccessor);
            }
            else
            {
                // Otherwise clone the current instance, so we have something to compare to
                try
                {
                    oldInfo = (PersonalInformation) newInfo.clone();
                }
                catch (CloneNotSupportedException e)
                {
                    throw new RemoteException("Error saving personal information: Clone not supported");
                }
            }

            if (newInfo.getId() != userInfo.getId())
                throw new RemoteException("Error saving personal information: ID cannot be changed");

            // If the values are the same, do nothing and exit
            if (newInfo.getContent().equals(userInfo.getContent()))
                return true;

            newInfo.setContent(userInfo.getContent());

            personalInformationManager.savePersonalInformation(newInfo, oldInfo);
        }

        return true;
    }

    public RemoteUserInformation getUserInformation(String username) throws RemoteException
    {
        User givenUser = retrieveUser(username);

        PersonalInformation info = personalInformationManager.getPersonalInformation(givenUser.getName());

        return info == null ? null : new RemoteUserInformation(info);
    }

    public boolean hasUser(String token, String username)
    {
        User user = userAccessor.getUser(username);

        // If the user exists, return true
        return (user != null);
    }

    public boolean hasGroup(String groupname)
    {
        Group group = userAccessor.getGroup(groupname);

        // If the group exists, return true
        return (group != null);
    }

    public PersonalInformationManager getPersonalInformationManager()
    {
        return personalInformationManager;
    }

    public void setPersonalInformationManager(PersonalInformationManager personalInformationManager)
    {
        this.personalInformationManager = personalInformationManager;
    }

    /**
     * Add a profile picture to a user's profile
     *
     * @param userName The user name of the profile
     * @param fileName File name of the picture
     * @param mimeType Image mime type (must be from image/*)
     * @param pictureData The image data
     * @return true if successful
     * @throws RemoteException
     */
    public boolean addProfilePicture(String userName, String fileName, String mimeType, byte[] pictureData) throws RemoteException {

        if (!mimeType.toLowerCase().startsWith("image/"))
            throw new RemoteException("Invalid MIME type. Only image/* types may be used for profile pictures");

        User user = userAccessor.getUser(userName);
        if (user == null)
            throw new RemoteException("User does not exist");

        User currentUser = AuthenticatedUserThreadLocal.getUser();

        // If the current user is not the given user, or they can't administrate
       if (!permissionManager.hasPermission(currentUser, Permission.EDIT, user))
            throw new NotPermittedException("You are not permitted to add a profile picture for the specified user");

        File resizedProfilePicture = new ProfilePicture(new ByteArrayInputStream(pictureData), fileName).create();

        if (resizedProfilePicture == null)
            throw new RemoteException("There was a problem resizing the image");

        PersonalInformation personalInfo = personalInformationManager.getPersonalInformation(userName);

        // TODO Most of this is duplicated from EditMyProfile::storeAttachment
        // Should be refactored somewhere else

        Attachment attachment = attachmentManager.getAttachment(personalInfo, fileName);
        Attachment previousVersion = null;

        if (attachment == null)
        {
            attachment = new Attachment();
        }
        else
        {
            try
            {
                previousVersion = (Attachment) attachment.clone();
            }
            catch (CloneNotSupportedException e)
            {
                throw new RemoteException("Error adding profile picture: Clone not supported");
            }
        }

        attachment.setContentType(mimeType);
        attachment.setFileName(fileName);
        attachment.setComment(Attachment.PROFILE_PICTURE_COMMENT);
        attachment.setFileSize(pictureData.length);
        personalInfo.addAttachment(attachment);

        try
        {
            attachmentManager.saveAttachment(attachment, previousVersion, new ByteArrayInputStream(pictureData));
        }
        catch (IOException e)
        {
            throw new RemoteException("Error adding profile picture: Cound not save attachment");
        }

        UserPreferences userPreferences = new UserPreferences(userAccessor.getPropertySet(user));
        try {
            userPreferences.setString(UserPreferencesKeys.PROPERTY_USER_PROFILE_PICTURE, fileName);
        } catch (AtlassianCoreException ex) {
            throw new RemoteException("Problem setting user preferences", ex);
        }

        return true;
    }
}