package com.atlassian.bitbucket.user;

import com.atlassian.bitbucket.ServerException;
import com.atlassian.bitbucket.auth.*;
import com.atlassian.bitbucket.avatar.AvatarSupplier;
import com.atlassian.bitbucket.avatar.CacheableAvatarSupplier;
import com.atlassian.bitbucket.comment.Comment;
import com.atlassian.bitbucket.project.PersonalProject;
import com.atlassian.bitbucket.pull.PullRequest;
import com.atlassian.bitbucket.util.Page;
import com.atlassian.bitbucket.util.PageRequest;

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

/**
 * Provides methods for querying {@link ApplicationUser users} and managing authentication. This service also offers limited
 * ability to update the current user.
 * <p>
 * A note about user stores and deleted users: the system uses Crowd to provide access to LDAP servers, other Crowd
 * servers, or JIRA (which is able to serve as a central user store for other Atlassian products). Crowd maintains
 * a local cache of users, which is termed the "user store" by this service. The system also maintains a secondary
 * table of users, outside Crowd, allowing it to decorate Crowd users with additional properties as well as to anchor
 * relationships between users and their data, such as {@link Comment comments} and
 * {@link PullRequest pull requests}.
 * <p>
 * Because the Crowd user store is a cache of users managed elsewhere, be they in LDAP, JIRA, etc., it is possible
 * for users to be deleted from that user store but still exist in the system's secondary user table. Such users are
 * termed "deleted", and, when retrieved, will only offer a minimal set of data. Generally, after deletion, only a
 * user's {@link ApplicationUser#getName() username} is still available. Display names, e-mail addresses and other data
 * are managed by Crowd and are lost when the user is deleted.
 * <p>
 * In addition to deleted users, Crowd has the concept of an inactive user. These users cannot log in and
 * may or may not have an associated {@code ApplicationUser}. Inactive users are handled consistently as deleted and
 * both are taken into consideration when determining {@link ApplicationUser#isActive()}.
 * <p>
 * Groups are reported as strings. When persisting or displaying groups, it is recommended to store the
 * group name as reported by the API; when performing tests for equality on groups, it is recommended to ignore case.
 *
 * @see UserAdminService
 */
public interface UserService {

    /**
     * Attempts to authenticate the specified user given their password. Failed authentications will not count towards
     * the maximum number allowed before CAPTCHA is imposed for the supplied user.
     *
     * @param username the username
     * @param password the user's password
     * @return the {@link ApplicationUser} representing the authenticated user
     * @throws AuthenticationSystemException if a failure occurs in Crowd
     * @throws ExpiredPasswordAuthenticationException if the user's password has expired and must be changed
     * @throws InactiveUserAuthenticationException if the specified user is inactive
     * @throws IncorrectPasswordAuthenticationException if the provided password is incorrect
     * @throws NoSuchUserException if the specified user does not exist or their user details cannot be retrieved
     * @see #unauthenticate()
     */
    @Nonnull
    ApplicationUser authenticate(@Nonnull String username, @Nonnull String password);

    /**
     * Attempts to authenticate the specified user given their password. Failed authentications will count towards
     * the maximum number allowed before CAPTCHA is imposed for the supplied user. If the maximum number of allowed
     * failed logins has already been exceeded the method will throw {@link CaptchaRequiredAuthenticationException}
     *
     * @param username the username
     * @param password the user's password
     * @return the {@link ApplicationUser} representing the authenticated user
     * @throws AuthenticationSystemException if a failure occurs in Crowd
     * @throws CaptchaRequiredAuthenticationException if CAPTCHA is required
     * @throws ExpiredPasswordAuthenticationException if the user's password has expired and must be changed
     * @throws InactiveUserAuthenticationException if the specified user is inactive
     * @throws IncorrectPasswordAuthenticationException if the provided password is incorrect
     * @throws NoSuchUserException if the specified user does not exist or their user details cannot be retrieved
     * @see #unauthenticate()
     * @since 4.2
     */
    @Nonnull
    ApplicationUser authenticateWithCaptcha(@Nonnull String username, @Nonnull String password);

    /**
     * Delete the avatar associated with a user.
     * <p>
     * This will revert the avatar of the user in the UI to his/her Gravatar image
     * (if the Gravatar integration is enabled) or to the default user avatar
     * (if the Gravatar integration is disabled)
     *
     * @param user the user whose (local) avatar will be removed
     */
    void deleteAvatar(@Nonnull ApplicationUser user);

    /**
     * Retrieves a flag indicating whether the specified group exists.
     *
     * @param groupName the group name
     * @return {@code true} if a group with the specified name exists in any directory; otherwise, {@code false}
     */
    boolean existsGroup(String groupName);

    /**
     * Retrieves a page of groups.
     * <p>
     * Note: To filter the retrieved groups by name, use {@link #findGroupsByName(String, PageRequest)} instead.
     *
     * @param pageRequest defines the page of groups to retrieve
     * @return the requested page of groups, which may be empty but never {@code null}
     * @see #findGroupsByName(String, PageRequest)
     */
    @Nonnull
    Page<String> findGroups(@Nonnull PageRequest pageRequest);

    /**
     * Retrieves a page of groups, optionally filtering the returned results to those containing the specified
     * {@code groupName}. If the provided {@code groupName} is {@code null}, this method behaves identically to
     * {@link #findGroups(PageRequest)}.
     *
     * @param groupName 0 or more characters to apply as a filter on returned groups
     * @param pageRequest defines the page of groups to retrieve
     * @return the requested page of groups, potentially filtered, which may be empty but never {@code null}
     * @see #findGroups(PageRequest)
     */
    @Nonnull
    Page<String> findGroupsByName(@Nullable String groupName, @Nonnull PageRequest pageRequest);

    /**
     * Retrieves a page of groups, optionally filtering the returned results to those beginning with the specified
     * {@code groupPrefix}. If the provided {@code groupPrefix} is {@code null}, this method behaves identically to
     * {@link #findGroups(PageRequest)}.
     * <p>
     * This method varies slightly from {@link #findGroupsByName(String, PageRequest)}, which will match the specified
     * name at the beginning of any word boundary in the group. For example, "test-group" will match "group". With this
     * method, a prefix of "group" will <i>not</i> match "test-group".
     *
     * @param groupPrefix 0 or more characters defining a <i>prefix</i> which returned group names must start with
     * @param pageRequest defines the page of groups to retrieve
     * @return the requested page of groups, potentially filtered by prefix, which may be empty but never {@code null}
     * @see #findGroups(PageRequest)
     */
    @Nonnull
    Page<String> findGroupsByPrefix(@Nullable String groupPrefix, @Nonnull PageRequest pageRequest);

    /**
     * Retrieves a page of groups which the specified user is a member of. The {@code username} provided must match
     * <i>exactly</i> in order for any results to be returned.
     *
     * @param username    the <i>exact</i> name of the user to retrieve groups for
     * @param pageRequest defines the page of groups to retrieve
     * @return the requested page of groups, which may be empty but never {@code null}
     * @see #findUsersByGroup(String, PageRequest)
     */
    @Nonnull
    Page<String> findGroupsByUser(@Nonnull String username, @Nonnull PageRequest pageRequest);

    /**
     * Retrieve the first {@link ApplicationUser#isActive() active} {@link UserType#NORMAL normal} user whose username or
     * e-mail address <i>exactly</i> matches the provided {@code value}.
     * <p>
     * Usernames are the preferred match, so they will be tested first. If no user exists with a username matching the
     * value, e-mail addresses will then be checked.
     *
     * @param value the value to match, first against usernames and then against e-mail addresses
     * @return a user whose username or e-mail address matches the specified value, or {@code null} if no matching user
     *         was found
     */
    @Nullable
    ApplicationUser findUserByNameOrEmail(@Nonnull String value);

    /**
     * Retrieves a page of {@link ApplicationUser#isActive() active} {@link UserType#NORMAL normal} users.
     * <p>
     * Note: To filter the retrieved users by name, use {@link #findUsersByName(String, PageRequest)} instead.
     *
     * @param pageRequest defines the page of users to retrieve
     * @return the requested page of users, which may be empty but never {@code null}
     * @see #findUsersByName(String, PageRequest)
     */
    @Nonnull
    Page<ApplicationUser> findUsers(@Nonnull PageRequest pageRequest);

    /**
     * Retrieves a page of {@link ApplicationUser#isActive() active} {@link UserType#NORMAL normal} users, optionally
     * filtering the returned results to those containing the specified {@code username}. If the provided
     * {@code username} is {@code null}, this method behaves identically to {@link #findUsers(PageRequest)}.
     * Otherwise, the {@code username} is matched against:
     * <ul>
     *     <li>{@link ApplicationUser#getDisplayName() Display names}</li>
     *     <li>{@link ApplicationUser#getEmailAddress() E-mail addresses}</li>
     *     <li>{@link ApplicationUser#getName() Usernames}</li>
     * </ul>
     *
     * @param username    0 or more characters to apply as a filter on returned users
     * @param pageRequest defines the page of users to retrieve
     * @return the requested page of users, potentially filtered, which may be empty but never {@code null}
     * @see #findUsers(PageRequest)
     */
    @Nonnull
    Page<ApplicationUser> findUsersByName(@Nullable String username, @Nonnull PageRequest pageRequest);

    /**
     * Retrieves a page of {@link ApplicationUser#isActive() active} users which are members of the specified group.
     * The {@code groupName} provided must match <i>exactly</i> in order for any results to be returned.
     *
     * @param groupName   the <i>exact</i> name of the group to retrieve members for
     * @param pageRequest defines the page of users to retrieve
     * @return the requested page of users, which may be empty but never {@code null}
     * @see #findGroupsByUser(String, PageRequest)
     */
    @Nonnull
    Page<ApplicationUser> findUsersByGroup(@Nonnull String groupName, @Nonnull PageRequest pageRequest);

    /**
     * Retrieves the current avatar for the specified user at a requested size. Avatars are square, so the size
     * provided here is used as both height and width for the avatar.
     * <p>
     * The requested size will be normalised to fall within a well-defined set sizes. The supported sizes are:
     * <ul>
     *     <li>256</li>
     *     <li>128</li>
     *     <li>96</li>
     *     <li>64</li>
     *     <li>48</li>
     * </ul>
     * Any size larger than 256 will be normalised down to 256, and any size smaller than 48 will be normalised up to
     * 48. Otherwise, sizes are normalised to the next size up, where they don't match exactly: 56 will be normalised
     * to 64, 100 will be normalised to 128, and so on.
     *
     * @param user the user whose avatar should be retrieved
     * @param size the desired height and width for the avatar
     * @return a supplier which can be used to access the requested avatar
     */
    @Nonnull
    CacheableAvatarSupplier getAvatar(@Nonnull ApplicationUser user, int size);

    /**
     * Retrieves an {@link ApplicationUser#isActive() active} {@link ServiceUser} by its {@link ApplicationUser#getName() username}.
     * <p>
     * This method will <i>not</i> return inactive users; use {@link #getServiceUserByName(String, boolean)} instead if
     * inactive users are desired. 
     *
     * @param username the username of the user to retrieve
     * @return the user with the specified username, or {@code null} if no matching user exists <i>or</i> the matching
     *         user has been deleted from the user store
     */
    @Nullable
    ServiceUser getServiceUserByName(@Nonnull String username);

    /**
     * Retrieves a {@link ServiceUser} by its {@link ApplicationUser#getName() username}, optionally returning the user even
     * if it has been marked inactive.
     * <p>
     * If requested, this method will return inactive users.
     *
     * @param username the username of the user to retrieve
     * @param inactive {@code true} if inactive users should be returned, {@code false} otherwise.
     * @return the user with the specified username, or {@code null} if no matching user exists <i>or</i> the matching
     *         user is inactive and inactive users were not requested
     */
    @Nullable
    ServiceUser getServiceUserByName(@Nonnull String username, boolean inactive);

    /**
     * Retrieves an {@link ApplicationUser#isActive() active} {@link ServiceUser} by its exact (case-sensitive)
     * {@link ApplicationUser#getSlug() slug}.
     * <p>
     * This method will <i>not</i> return inactive users.
     *
     * @param slug the slug of the user to retrieve
     * @return the user with the specified slug, or {@code null} if no matching user exists <i>or</i> the matching
     *         user is inactive
     */
    @Nullable
    ServiceUser getServiceUserBySlug(@Nonnull String slug);

    /**
     * Retrieves a {@link ApplicationUser} by its {@link ApplicationUser#getId() ID}.
     * <p>
     * This method will <i>not</i> return deleted or inactive users;
     * use {@link #getUserById(int, boolean)} instead if deleted users are desired.
     * See the class documentation for more details about what "deleted" means in this context.
     *
     * @param id the ID of the user to retrieve
     * @return the user with the specified ID, or {@code null} if no matching user exists
     */
    @Nullable
    ApplicationUser getUserById(int id);

    /**
     * Retrieves a {@link ApplicationUser} by its {@link ApplicationUser#getId() ID}.
     * <p>
     * If requested, this method will return deleted or inactive users.
     * See the class documentation for more details about what "deleted" means in this context.
     *
     * @param id the ID of the user to retrieve
     * @param inactive {@code true} if deleted and inactive users should be returned, {@code false} otherwise.
     * @return the user with the specified ID, or {@code null} if no matching user exists
     */
    @Nullable
    ApplicationUser getUserById(int id, boolean inactive);

    /**
     * Retrieves an {@link ApplicationUser#isActive() active} {@link ApplicationUser} by its {@link ApplicationUser#getName() username}.
     * <p>
     * This method will <i>not</i> return deleted or inactive users; use {@link #getUserByName(String, boolean)} instead if
     * deleted users are desired. See the class documentation for more details about what "deleted" means in this
     * context.
     *
     * @param username the username of the user to retrieve
     * @return the user with the specified username, or {@code null} if no matching user exists <i>or</i> the matching
     *         user has been deleted from the user store
     */
    @Nullable
    ApplicationUser getUserByName(@Nonnull String username);

    /**
     * Retrieves a {@link ApplicationUser} by its {@link ApplicationUser#getName() username}, optionally returning the user even
     * if it has been removed from the underlying user store.
     * <p>
     * If requested, this method will return deleted or inactive users.
     * See the class documentation for more details about what "deleted" means in this context.
     *
     * @param username the username of the user to retrieve
     * @param inactive {@code true} if deleted and inactive users should be returned, {@code false} otherwise.
     * @return the user with the specified username, or {@code null} if no matching user exists <i>or</i> the matching
     *         user has been deleted from the user store and deleted users were not requested
     */
    @Nullable
    ApplicationUser getUserByName(@Nonnull String username, boolean inactive);

    /**
     * Retrieves a set of {@link ApplicationUser#isActive() active} {@link ApplicationUser}s by their {@link ApplicationUser#getId() IDs}.
     * <p>
     * This method will <i>not</i> return deleted or inactive users; use {@link #getUsersById(Set, boolean)}
     * instead if deleted users are desired.
     * See the class documentation for more details about what "deleted" means in this context.
     *
     * @param ids the IDs of the users to retrieve
     * @return a set of non-deleted users
     */
    @Nonnull
    Set<ApplicationUser> getUsersById(@Nonnull Set<Integer> ids);

    /**
     * Retrieves a set of {@link ApplicationUser}s by their {@link ApplicationUser#getId() IDs}, optionally including users who
     * have been removed from the underlying user store.
     * <p>
     * If requested, this method will return deleted users. See the class documentation for more details about what
     * "deleted" means in this context.
     *
     * @param ids     the IDs of the users to retrieve
     * @param inactive {@code true} if deleted and inactive users should be returned, {@code false} otherwise
     * @return users matching IDs from the specified set, if any
     */
    @Nonnull
    Set<ApplicationUser> getUsersById(@Nonnull Set<Integer> ids, boolean inactive);

    /**
     * Retrieves an {@link ApplicationUser#isActive() active} {@link ApplicationUser} by its
     * exact (case-sensitive) {@link ApplicationUser#getSlug() slug}.
     * <p>
     * This method will <i>not</i> return deleted or inactive users. See the class documentation for more details about what
     * "deleted" means in this context.
     *
     * @param slug the slug of the user to retrieve
     * @return the user with the specified slug, or {@code null} if no matching user exists <i>or</i> the matching
     *         user has been deleted or is inactive
     */
    @Nullable
    ApplicationUser getUserBySlug(@Nonnull String slug);

    /**
     * Retrieves a set of {@link ApplicationUser#isActive() active} {@link ApplicationUser}s by
     * their {@link ApplicationUser#getName() usernames}.
     * <p>
     * This method will <i>not</i> return deleted or inactive users;
     * use {@link #getUsersByName(Set, boolean)} instead if deleted
     * users are desired. See the class documentation for more details about what "deleted" means in this context.
     *
     * @param usernames the usernames of the users to retrieve
     * @return users matching usernames from the specified set, if any
     */
    @Nonnull
    Set<ApplicationUser> getUsersByName(@Nonnull Set<String> usernames);

    /**
     * Retrieves a set of {@link ApplicationUser}s by their {@link ApplicationUser#getName() usernames}, optionally including users
     * who have been removed from the underlying user store.
     * <p>
     * If requested, this method will return deleted users. See the class documentation for more details about what
     * "deleted" means in this context.
     *
     * @param usernames the usernames of the users to retrieve
     * @param inactive {@code true} if deleted and inactive users should be returned, {@code false} otherwise
     * @return users matching usernames from the specified set, if any
     */
    @Nonnull
    Set<ApplicationUser> getUsersByName(@Nonnull Set<String> usernames, boolean inactive);

    /**
     * Retrieves a flag indicating whether the specified user is a member of the specified group. If the provided user
     * does not exist in the user store, or if the group does not exist, {@code false} is returned in preference to
     * throwing an exception.
     *
     * @param user      the user to query group membership for
     * @param groupName the <i>exact</i> name of the group to query membership in
     * @return {@code true} if the specified user is a member of the specified group; otherwise, {@code false} if the
     *         user does not exist in the backing store, the group does not exist or the user is not a member of the
     *         group
     */
    boolean isUserInGroup(@Nonnull ApplicationUser user, @Nonnull String groupName);

    /**
     * Retrieves a flag indicating whether the specified user is a member of the specified group. If the user or group
     * does not exist, {@code false} is returned in preference to throwing an exception.
     *
     * @param username  the <i>exact</i> name of the user to query group membership for
     * @param groupName the <i>exact</i> name of the group to query membership in
     * @return {@code true} if the specified user is a member of the specified group; otherwise, {@code false} if the
     *         user does not exist, the group does not exist or the user is not a member of the group
     */
    boolean isUserInGroup(@Nonnull String username, @Nonnull String groupName);

    /**
     * Authenticate as user {@code username}, without checking passwords. Should only be used for trusted logins.
     * @param username the username
     * @return the {@link ApplicationUser} representing the authenticated user.
     */
    @Nullable
    ApplicationUser preauthenticate(@Nonnull String username);

    /**
     * Searches for {@link ApplicationUser users} that match the provided {@link UserSearchRequest request}.
     *
     * @param request     a request object describing the users to return
     * @param pageRequest the bounds of the page
     * @return a page containing 0 or more {@link ApplicationUser users} that match the provided
     *         {@link UserSearchRequest criteria}
     * @see UserSearchRequest
     */
    @Nonnull
    Page<ApplicationUser> search(@Nonnull UserSearchRequest request, @Nonnull PageRequest pageRequest);

    /**
     * Clears the {@link #authenticate(String, String) authentication} for the current user, logging
     * them out.
     */
    void unauthenticate();

    /**
     * Updates the specified user's avatar, replacing it with the one contained in the provided
     * {@link AvatarSupplier supplier}. Updating a user's avatar also updates the avatar for their
     * {@link PersonalProject personal project}.
     * <p>
     * Previous avatars <i>are not retained</i>. When a user's avatar is updated, the previous avatar is removed.
     * To reuse a previous avatar, it must be provided again.
     *
     * @param user     the user to update the avatar for
     * @param supplier a supplier providing access to the new avatar to use
     */
    void updateAvatar(@Nonnull ApplicationUser user, @Nonnull AvatarSupplier supplier);

    /**
     * Updates the specified user's avatar, replacing it with the one contained in the provided data URI. Updating a
     * user's avatar also updates the avatar for their {@link PersonalProject personal
     * project}.
     * <p>
     * The data URI is required to contain Base64-encoded image data, and should be in the format:
     * <code>
     *     data:(content type, e.g. image/png);base64,(data)
     * </code>
     * If the data is not Base64-encoded, or if a character set is defined in the URI, it will be rejected.
     * <p>
     * Previous avatars <i>are not retained</i>. When a project's avatar is updated, the previous avatar is removed.
     * To reuse a previous avatar, it must be provided again.
     *
     * @param user the user to update the avatar for
     * @param uri  a data URI containing a Base64-encoded avatar image
     */
    void updateAvatar(@Nonnull ApplicationUser user, @Nonnull String uri);

    /**
     * Updates the password for the <i>current user</i>.
     * <p>
     * The current user always has permission to modify their own password. However, the underlying user store may not
     * support the operation, or it may apply specific requirements to the complexity of the new password. If the user
     * store is read-only, or the password does not meet complexity requirements, a {@link ServerException} is thrown.
     *
     * @param currentPassword the current user's current password
     * @param newPassword     the current user's desired new password
     * @throws IncorrectPasswordAuthenticationException if the current password provided does not match the user's
     *                                                  current password
     * @throws ServerException if the underlying user store does not support updating users' passwords
     */
    void updatePassword(@Nonnull String currentPassword, @Nonnull String newPassword)
            throws IncorrectPasswordAuthenticationException, ServerException;

    /**
     * Updates the {@link ApplicationUser#getDisplayName() display name} and {@link ApplicationUser#getEmailAddress()
     * e-mail address} for the <i>current user</i>.
     * <p>
     * The current user always has permission to modify their own details. However, the underlying user store may not
     * support the operation. A {@link ServerException} will be thrown if the user store is read-only.
     *
     * @param displayName  the new display name for the current user
     * @param emailAddress the new e-mail address for the current user
     * @return the current user, with the updated details
     * @throws ServerException if the underlying user store does not support updating users' details
     */
    @Nonnull
    ApplicationUser updateUser(@Nonnull String displayName, @Nonnull String emailAddress) throws ServerException;
}
