package com.atlassian.stash.internal.crowd;

import com.atlassian.bitbucket.ForbiddenException;
import com.atlassian.crowd.embedded.api.Directory;
import com.atlassian.crowd.embedded.api.Group;
import com.atlassian.crowd.embedded.api.User;
import com.atlassian.crowd.embedded.api.UserCapabilities;
import com.atlassian.crowd.embedded.api.UserWithAttributes;
import com.atlassian.crowd.search.query.entity.restriction.Property;
import com.atlassian.bitbucket.IntegrityException;
import com.atlassian.bitbucket.user.NoSuchGroupException;
import com.atlassian.bitbucket.user.NoSuchUserException;
import com.atlassian.bitbucket.ServerException;
import com.atlassian.bitbucket.auth.AuthenticationSystemException;
import com.atlassian.bitbucket.auth.ExpiredPasswordAuthenticationException;
import com.atlassian.bitbucket.auth.InactiveUserAuthenticationException;
import com.atlassian.bitbucket.auth.IncorrectPasswordAuthenticationException;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.List;

/**
 * Aim: To provide a slightly less obtuse Crowd API than Crowd does.
 * <p>
 * All Crowd-level exceptions are handled and translated in some fashion, whether they are checked or unchecked. The
 * handling is applied in a way which is (generally) consistent across all methods. That means most methods on this
 * interface can <i>conceivably</i> throw:
 * <ul>
 *     <li>{@link NoSuchGroupException}, for any call which references a group, whether by name or {@code Group}
 *     instance, where the specified group is not found</li>
 *     <li>{@link NoSuchUserException}, for any call which references a user, whether by name or {@code User} instance,
 *     where the specified user is not found</li>
 *     <li>{@link AuthenticationSystemException} for any otherwise-uncategorised failure of a Crowd service call</li>
 * </ul>
 *
 * @since 2.0
 */
public interface CrowdControl {

    /**
     * Adds the provided {@code user} to the specified {@code group}.
     *
     * @param group the group to add the user to
     * @param user  the user to add to the group
     * @throws AuthenticationSystemException if the Crowd operation fails
     * @throws NoSuchGroupException if the specified {@code Group} does not exist in Crowd
     * @throws NoSuchUserException if the specified {@code User} does not exist in Crowd
     */
    void addGroupMember(@Nonnull Group group, @Nonnull User user);

    /**
     * Attempts to authenticate a {@code User} with the specified {@code username} with the provided {@code password}.
     * <p>
     * This service method does not apply CAPTCHA-related considerations. It deals purely with Crowd itself, not with
     * the Stash-specific configuration around how many failed authentication attempts triggers requiring a CAPTCHA.
     *
     * @param username the username of the user to authenticate
     * @param password the password for the user
     * @return the authenticated user
     * @throws ExpiredPasswordAuthenticationException if the user's credentials in Crowd have expired
     * @throws InactiveUserAuthenticationException if the specified user is inactive in Crowd
     * @throws IncorrectPasswordAuthenticationException if the provided {@code password} does not match the credentials
     *                                                  stored in Crowd for the specified user
     * @throws NoSuchUserException if no user exists in Crowd with the specified {@code username}, or if the user's
     *                             details are not returned by Crowd
     */
    @Nonnull
    User authenticate(@Nonnull String username, @Nonnull String password);

    /**
     * Checks whether the user's password can be locally reset.
     * <p>
     * Only users from internal directories (excluding internal directories with delegated LDAP authentication) can
     * reset their passwords through Stash. Users from remote directories (LDAP, Crowd, etc.) need to update their
     * passwords using their providers' user interface or API.
     *
     * @param username the username of the user
     * @return whether the user can reset his/her password in Stash
     */
    boolean canResetPassword(@Nonnull String username);

    /**
     * Creates the provided {@code group}.
     *
     * @param group the group to create
     * @return the created group
     * @throws IntegrityException if a group with the specified name already exists in Crowd
     */
    @Nonnull
    Group createGroup(@Nonnull Group group);

    /**
     * Creates the provided {@code user} with the specified password.
     *
     * @param user     the user to create
     * @param password the initial password for the user
     * @return the created user
     * @throws IntegrityException if a user with the same {@code User.getName()} already exists in Crowd
     */
    @Nonnull
    User createUser(@Nonnull User user, @Nonnull String password);

    /**
     * Deletes the specified {@code group}.
     *
     * @param group the group to delete
     * @return {@code true} if the specified group was successfully deleted; otherwise, {@code false}
     */
    boolean deleteGroup(@Nonnull Group group);

    /**
     * Deletes the specified {@code user}.
     *
     * @param user the user to delete
     * @return {@code true} if the specified user was successfully deleted; otherwise, {@code false}
     */
    boolean deleteUser(@Nonnull User user);

    /**
     * Retrieves the {@code Directory} in which the provided {@code User} exists.
     * <p>
     * Because the user was loaded from Crowd, theoretically this should never return {@code null}. However, it is not
     * specifically precluded by Crowd, so callers should handle the situation where the directory is not available.
     *
     * @param user the user to retrieve the directory for
     * @return the directory in which the specified {@code user} exists
     */
    @Nullable
    Directory findDirectoryFor(@Nonnull User user);

    /**
     * Attempts to retrieve the group with the specified {@code groupName}.
     *
     * @param groupName the <i>exact</i> name of the group to retrieve
     * @return the named group, or {@code null} if no such group exists
     */
    @Nullable
    Group findGroup(@Nonnull String groupName);

    /**
     * Retrieves an <i>unfiltered</i> page of groups.
     *
     * @param pageRequest defines the page of groups to retrieve
     * @return a page of groups, which may be empty but never {@code null}
     */
    @Nonnull
    Page<String> findGroups(@Nonnull PageRequest pageRequest);

    /**
     * Retrieves a page of groups filtered by name. If no {@code groupName} is provided the query is optimised to
     * behave identically to {@link #findGroups(PageRequest)}.
     *
     * @param groupName   0 or more characters to apply as a "containing" filter on returned groups
     * @param pageRequest defines the page of groups to retrieve
     * @return a page of groups, optionally filtered by the provided {@code groupName}, which may be empty but never
     *         {@code null}
     */
    @Nonnull
    Page<String> findGroupsByName(@Nullable String groupName, @Nonnull PageRequest pageRequest);

    /**
     * Retrieves a page of groups filtered by prefix. If no {@code groupPrefix} is provided the query is optimised to
     * behave identically to {@link #findGroups(PageRequest)}.
     *
     * @param groupPrefix 0 or more characters to apply as a "startingWith" filter on returned groups
     * @param pageRequest defines the page of groups to retrieve
     * @return a page of groups, optionally filtered by the provided {@code groupPrefix}, which may be empty but never
     *         {@code null}
     */
    @Nonnull
    Page<String> findGroupsByPrefix(@Nullable String groupPrefix, @Nonnull PageRequest pageRequest);

    /**
     * Retrieves a page of groups to which the specified user belongs, or does not belong if {@code invert}ed,
     * optionally filtered by {@code groupName}. If no user exists with the specified {@code username}, no exception
     * is thrown but the returned {@link Page page} will always be empty.
     *
     * @param username    the <i>exact</i> name of the user to retrieve groups for
     * @param groupName   0 or more characters to apply as a filter on returned groups
     * @param invert      {@code true} to retrieve groups of which the user <i>is not</i> a member; otherwise,
     *                    {@code false} to retrieve groups of which the user <i>is</i> a member
     * @param pageRequest defines the page of groups to retrieve
     * @return a page of groups which the user is, or is not, a member of, optionally filtered by the provided
     *         {@code groupName}, which may be empty but never {@code null}
     */
    @Nonnull
    Page<String> findGroupsByUser(@Nonnull String username, @Nullable String groupName, boolean invert,
                                  @Nonnull PageRequest pageRequest);

    /**
     * Attempts to retrieve the user with the specified {@code username}.
     *
     * @param username the <i>exact</i> name of the user to retrieve
     * @param inactive {@code false} if only {@link User#isActive() active} users should be considered,
     * {@code true} to consider all users
     * @return the named user, or {@code null} if no such user exists
     */
    @Nullable
    User findUser(@Nonnull String username, boolean inactive);

    /**
     * Attempts to retrieve the first {@link User#isActive() active} user with the given {@code property} exactly matching a {@code value}
     *
     * @param property the property to restrict the search by
     * @param value the value of the property
     * @param <T> the type of the value
     * @return the first active user matching the search or null if none match.
     */
    @Nullable
    <T> User findUserByProperty(Property<T> property, T value);

    /**
     * Retrieves a page of {@link User#isActive() active} users.
     *
     * @param pageRequest defines the page of users to retrieve
     * @return a page of users, which may be empty but never {@code null}
     */
    @Nonnull
    Page<User> findUsers(@Nonnull PageRequest pageRequest);

    /**
     * Retrieves a page of {@link User#isActive() active} users which belong to the specified group,
     * or do not belong if {@code invert}ed, optionally filtered by {@code username}.
     * If no group exists with the specified {@code groupName}, no
     * exception is thrown but the returned {@link Page page} will always be empty.
     *
     * @param groupName   the <i>exact</i> name of the group to retrieve users for
     * @param username    0 or more characters to apply as a filter on returned users
     * @param invert      {@code true} to retrieve users which <i>are not</i> members of the group; otherwise,
     *                    {@code false} to retrieve users which <i>are</i> members of the group
     * @param pageRequest defines the page of users to retrieve
     * @return a page of users which are, or are not, members of the group, optionally filtered by the provided
     *         {@code username}, which may be empty but never {@code null}
     */
    @Nonnull
    Page<User> findUsersByGroup(@Nonnull String groupName, @Nullable String username, boolean invert,
                                @Nonnull PageRequest pageRequest);

    /**
     * Retrieves a page of {@link User#isActive() active} users filtered by name.
     * If no {@code username} is provided the query is optimised to behave
     * identically to {@link #findUsers(PageRequest)}
     *
     * @param username    0 or more characters to apply as a filter on returned users
     * @param pageRequest defines the page of users to retrieve
     * @return a page of users, optionally filtered by the provided {@code username}, which may be empty but never
     *         {@code null}
     */
    @Nonnull
    Page<User> findUsersByName(@Nullable String username, @Nonnull PageRequest pageRequest);

    /**
     * Attempts to retrieve the {@link User#isActive() active} user details <i>and attributes</i>
     * for the specified {@code username}.
     *
     * @param username the <i>exact</i> name of the user to retrieve attributes for
     * @return the named user, with its attributes, or {@code null} if no such user exists
     */
    @Nullable
    UserWithAttributes findUserWithAttributes(@Nonnull String username);

    /**
     * Retrieves the specified group, throwing a {@link NoSuchGroupException} if no such group exists.
     *
     * @param groupName the <i>exact</i> name of the group to retrieve
     * @return the named group
     * @throws NoSuchGroupException if no group exists with the specified name
     */
    @Nonnull
    Group getGroup(@Nonnull String groupName);

    /**
     * Retrieves the specified user, throwing a {@link NoSuchUserException} if no such user exists or
     * it is not {@link User#isActive() active}.
     *
     * @param username the <i>exact</i> name of the user to retrieve
     * @return the named user
     * @throws NoSuchUserException if no user exists with the specified name
     */
    @Nonnull
    User getUser(@Nonnull String username);

    /**
     * Retrieves a flag indicating whether the provided {@code user} is a member of the specified group.
     * <p>
     * If the directory for the user cannot be retrieved, rather than propagating the exception thrown by Crowd
     * {@code false} is returned instead.
     *
     * @param groupName the <i>exact</i> name of the group to consider membership in
     * @param user      the user to check membership for
     * @return {@code true} if the user is a member of the specified group; otherwise, {@code false} if the user is not
     *         a member of the group or the directory containing the user cannot be found
     */
    boolean isGroupMember(@Nonnull String groupName, @Nonnull User user);

    /**
     * Retrieves a flag indicating whether the provided {@code childGroupName} is a member of
     * (i.e. nested within) the specified {@code parentGroupName}.
     *
     * @param childGroupName name of child group to inspect
     * @param parentGroupName name of parent group to inspect
     * @return {@code true} if and only if the {@code childGroupName} is a direct or indirect (nested) member of the {@code parentGroupName}.
     *         If any of the groups cannot be found, then {@code false} is returned.
     */
    boolean isGroupMemberOfGroup(@Nonnull String childGroupName, @Nonnull String parentGroupName);

    /**
     * Lists all of the directories which have been configured in Crowd. Any ordering applied to the list is a Crowd
     * implementation detail, and not part of the contract of this service; calling code should treat the list as if
     * it were in random order. Additionally, the returned list may include inactive directories.
     * <p>
     * Note: The system creates a single internal {@code Directory} when it is first started. Administrators may
     * configure additional directories but there will always be at least one <i>active</i> {@code Directory} in
     * the returned list; it is not possible to disable all directories.
     *
     * @return a list containing at least one active directory
     */
    @Nonnull
    List<Directory> listDirectories();

    /**
     * Removes the provided {@code user} from the specified {@code group}.
     * <p>
     * When multiple directories are configured in Crowd, it is possible for groups with the same name to exist in
     * multiple directories, and also possible for multiple users with the same name to exist in multiple directories.
     * These duplicates are treated as the same groups and users, and generally that works well. However, it can lead
     * to unexpected behaviour for this method. Consider the following scenario:
     * <ul>
     *     <li>User "someuser" exists in two directories, A and B</li>
     *     <li>In directory A, "someuser" is a member of "somegroup"</li>
     *     <li>In directory B, "someuser" is <i>not</i> a member of "somegroup" (there may not even be a group with
     *     that name in directory B)</li>
     * </ul>
     * If the directory traversal order when looking up "someuser" by name returns the user from directory B, attempting
     * to remove "someuser" from "somegroup" (which {@link #findGroupsByUser(String, String, boolean, PageRequest)} will
     * indicate "someuser" is a member of) will return {@code false}, because the Crowd implementation removes the user
     * from the named group within the same directory. To further complicate matters, Crowd offers no way to detect when
     * this occurs, since its implementation merely returns {@code true} or {@code false}. That, in turn, means there is
     * no real way for calling code to detect this case either.
     * <p>
     * Note: If "somegroup" didn't exist <i>at all</i> in directory B, in this example, {@link NoSuchGroupException}
     * would be thrown <i>even though {@link #findGroups(PageRequest)} would return "somegroup" as existing</i>.
     *
     * @param group the group to remove the user from
     * @param user  the user to remove from the group
     * @return {@code true} if the user has been removed from the group; otherwise, {@code false} if the user was
     *         not a member of the group (and therefore not removed)
     * @throws AuthenticationSystemException if the directory containing the user does not allow modifying the user's
     *                                       groups, or if some other random Crowd failure prevents updating them
     * @throws NoSuchGroupException if the specified group does not exist in Crowd
     * @throws NoSuchUserException if the specified user does not exist in Crowd
     */
    boolean removeGroupMember(@Nonnull Group group, @Nonnull User user);

    /**
     * Removes the named attribute from the specified {@code user}.
     *
     * @param user          the user to remove the attribute from
     * @param attributeName the name of the attribute to remove
     * @throws AuthenticationSystemException if the directory storing the user has not been configured to allow user
     *                                       attributes to be removed, or some other random Crowd failure occurs
     * @throws NoSuchUserException if the specified user does not exist in Crowd
     */
    void removeUserAttribute(@Nonnull User user, @Nonnull String attributeName);

    /**
     * Change the username of the specified {@code user}.
     *
     * @param user        the user to rename
     * @param newUsername the target username
     * @return the renamed uer
     * @throws IntegrityException if a user with the username {@code newUsername} already exists
     * @throws ForbiddenException
     *                            if the directory the specified {@code user}
     *                            exists within does not support renaming
     * @since 2.9
     */
    User renameUser(@Nonnull User user, @Nonnull String newUsername);

    /**
     * Sets the password for the specified {@code user}.
     *
     * @param user     the user whose password should be updated
     * @param password the new password for the user
     * @throws NoSuchUserException if the specified user does not exist in Crowd
     * @throws ServerException if the directory containing the user is not mutable, or if some other random Crowd
     *                         failure prevents the password from being set
     */
    void setPassword(@Nonnull User user, @Nonnull String password);

    /**
     * Sets the named attribute on the specified {@code user}.
     * <p>
     * Attributes <i>cannot be removed</i> with this method. Passing a {@code null} {@code attributeValue} will trigger
     * a {@code NullPointerException}; it will not attempt to remove the attribute. To remove attributes, call
     * {@link #removeUserAttribute(User, String)} instead.
     * <p>
     * Note: If the directory storing the user has not been configured to allow setting attributes, the exception from
     * Crowd is <i>logged and ignored</i>, rather than being translated and rethrown.
     *
     * @param user           the user to set the attribute for
     * @param attributeName  the name of the attribute to set
     * @param attributeValue the value to assign to the attribute
     * @throws NoSuchUserException if the specified user does not exist in Crowd
     */
    void setUserAttribute(@Nonnull User user, @Nonnull String attributeName, @Nonnull Object attributeValue);

    /**
     * Updates the display name and e-mail address for the specified user.
     *
     * @param user the user to update, providing both the username used to bind the updated values to the target and
     *             the new display name and e-mail address to apply
     * @return the updated user
     * @throws NoSuchUserException if the specified user does not exist in Crowd; they will not be created
     * @throws ServerException if the directory containing the user is not mutable, or if some other random Crowd
     *                         failure prevents the password from being set
     */
    @Nonnull
    User updateUser(@Nonnull User user);

    /**
     * Returns the Crowd user capabilities for a new user
     *
     * @return Crowd UserCapabilities for a new user
     * @since 3.8
     */
    @Nonnull
    UserCapabilities getCapabilitiesForNewUsers();
}
