# -*- coding: utf-8 -*-
"""Module that provides functionality for user manipulation."""
 
from AccessControl.Permission import getPermissions
from Products.CMFPlone.RegistrationTool import get_member_by_login_name
from contextlib import contextmanager
from plone.api import env
from plone.api import portal
from plone.api.exc import GroupNotFoundError
from plone.api.exc import InvalidParameterError
from plone.api.exc import MissingParameterError
from plone.api.exc import UserNotFoundError
from plone.api.validation import at_least_one_of
from plone.api.validation import mutually_exclusive_parameters
from plone.api.validation import required_parameters
 
import random
import string
 
 
def create(
    email=None,
    username=None,
    password=None,
    roles=('Member', ),
    properties=None
):
    """Create a user.
 
    :param email: [required] Email for the new user.
    :type email: string
    :param username: Username for the new user. This is required if email
        is not used as a username.
    :type username: string
    :param password: Password for the new user. If it's not set we generate
        a random 8-char alpha-numeric one.
    :type password: string
    :param properties: User properties to assign to the new user. The list of
        available properties is available in ``portal_memberdata`` through ZMI.
    :type properties: dict
    :returns: Newly created user
    :rtype: MemberData object
    :raises:
        MissingParameterError
        InvalidParameterError
    :Example: :ref:`user_create_example`
    """
    if properties is None:
        # Never use a dict as default for a keyword argument.
        properties = {}
 
    # it may happen that someone passes email in the properties dict, catch
    # that and set the email so the code below this works fine
    if not email and properties.get('email'):
        email = properties.get('email')
 
    if not email:
        raise MissingParameterError("You need to pass the new user's email.")
 
    site = portal.get()
    props = site.portal_properties
    use_email_as_username = props.site_properties.use_email_as_login
 
    if not use_email_as_username and not username:
        raise InvalidParameterError(
            "The portal is configured to use username "
            "that is not email so you need to pass a username."
        )
 
    registration = portal.get_tool('portal_registration')
    user_id = use_email_as_username and email or username
 
    # Generate a random 8-char password
    if not password:
        chars = string.ascii_letters + string.digits
        password = ''.join(random.choice(chars) for x in range(8))
 
    properties.update(username=user_id)
    properties.update(email=email)
 
    registration.addMember(
        user_id,
        password,
        roles,
        properties=properties
    )
    return get(username=user_id)
 
 
@mutually_exclusive_parameters('userid', 'username')
@at_least_one_of('userid', 'username')
def get(userid=None, username=None):
    """Get a user.
 
    Plone provides both a unique, unchanging identifier for a user (the
    userid) and a username, which is the value a user types into the login
    form. In many cases, the values for each will be the same, but under some
    circumstances they will differ. Known instances of this behavior include:
 
     * using content-based members via membrane
     * users changing their email address when using email as login is enabled
 
    We provide the ability to look up users by either.
 
    :param userid: Userid of the user we want to get.
    :type userid: string
    :param username: Username of the user we want to get.
    :type username: string
    :returns: User
    :rtype: MemberData object
    :raises:
        MissingParameterError
    :Example: :ref:`user_get_example`
    """
    if userid is not None:
        portal_membership = portal.get_tool('portal_membership')
        return portal_membership.getMemberById(userid)
 
    return get_member_by_login_name(
        portal.get(),
        username,
        raise_exceptions=False
    )
 
 
def get_current():
    """Get the currently logged-in user.
 
    :returns: Currently logged-in user
    :rtype: MemberData object
    :Example: :ref:`user_get_current_example`
    """
    portal_membership = portal.get_tool('portal_membership')
    return portal_membership.getAuthenticatedMember()
 
 
@mutually_exclusive_parameters('groupname', 'group')
def get_users(groupname=None, group=None):
    """Get all users or all users filtered by group.
 
    Arguments ``group`` and ``groupname`` are mutually exclusive.
    You can either set one or the other, but not both.
 
    :param groupname: Groupname of the group of which to return users. If set,
        only return users that are member of this group.
    :type username: string
    :param group: Group of which to return users.
        If set, only return users that are member of this group.
    :type group: GroupData object
    :returns: All users (optionlly filtered by group)
    :rtype: List of MemberData objects
    :Example: :ref:`user_get_all_users_example`,
        :ref:`user_get_groups_users_example`
    """
    if groupname:
        group_tool = portal.get_tool('portal_groups')
        group = group_tool.getGroupById(groupname)
        if not group:
            raise GroupNotFoundError
 
    portal_membership = portal.get_tool('portal_membership')
 
    if group:
        return group.getGroupMembers()
    else:
        return portal_membership.listMembers()
 
 
@mutually_exclusive_parameters('username', 'user')
@at_least_one_of('username', 'user')
def delete(username=None, user=None):
    """Delete a user.
 
    Arguments ``username`` and ``user`` are mutually exclusive. You can either
    set one or the other, but not both.
 
    :param username: Username of the user to be deleted.
    :type username: string
    :param user: User object to be deleted.
    :type user: MemberData object
    :raises:
        MissingParameterError
        InvalidParameterError
    :Example: :ref:`user_delete_example`
    """
    portal_membership = portal.get_tool('portal_membership')
    user_id = username or user.id
    portal_membership.deleteMembers((user_id,))
 
 
def is_anonymous():
    """Check if the currently logged-in user is anonymous.
 
    :returns: True if the current user is anonymous, False otherwise.
    :rtype: bool
    :Example: :ref:`user_is_anonymous_example`
    """
    return bool(portal.get_tool('portal_membership').isAnonymousUser())
 
 
@mutually_exclusive_parameters('username', 'user')
def get_roles(username=None, user=None, obj=None, inherit=True):
    """Get user's site-wide or local roles.
 
    Arguments ``username`` and ``user`` are mutually exclusive. You
    can either set one or the other, but not both. if ``username`` and
    ``user`` are not given, the currently authenticated member will be used.
 
    :param username: Username of the user for which to get roles.
    :type username: string
    :param user: User object for which to get roles.
    :type user: MemberData object
    :param obj: If obj is set then return local roles on this context.
        If obj is not given, the site root local roles will be returned.
    :type obj: content object
    :param inherit: if obj is set and inherit is False, only return
        local roles
    :type inherit: bool
    :raises:
        MissingParameterError
    :Example: :ref:`user_get_roles_example`
    """
    portal_membership = portal.get_tool('portal_membership')
 
    if username is None:
        if user is None:
            username = portal_membership.getAuthenticatedMember().getId()
        else:
            username = user.getId()
 
    user = portal_membership.getMemberById(username)
    if user is None:
        raise UserNotFoundError
 
    if obj is not None:
        if inherit:
            return user.getRolesInContext(obj)
        else:
            return obj.get_local_roles_for_userid(username)
    else:
        return user.getRoles()
 
 
@contextmanager
def _nop_context_manager():
    """A trivial context maanger that does nothing."""
    yield
 
 
@mutually_exclusive_parameters('username', 'user')
def get_permissions(username=None, user=None, obj=None):
    """Get user's site-wide or local permissions.
 
    Arguments ``username`` and ``user`` are mutually exclusive. You
    can either set one or the other, but not both. if ``username`` and
    ``user`` are not given, the authenticated member will be used.
 
    :param username: Username of the user for which you want to check
        the permissions.
    :type username: string
    :param user: User object for which you want to check the permissions.
    :type user: MemberData object
    :param obj: If obj is set then check the permissions on this context.
        If obj is not given, the site root will be used.
    :type obj: content object
    :raises:
        InvalidParameterError
    :Example: :ref:`user_get_permissions_example`
    """
    if obj is None:
        obj = portal.get()
 
    if username is None and user is None:
        context = _nop_context_manager()
    else:
        context = env.adopt_user(username, user)
 
    with context:
        adopted_user = get_current()
        permissions = (p[0] for p in getPermissions())
        d = {}
        for permission in permissions:
            d[permission] = bool(adopted_user.checkPermission(permission, obj))
 
    return d
 
 
@required_parameters('roles')
@mutually_exclusive_parameters('username', 'user')
def grant_roles(username=None, user=None, obj=None, roles=None):
    """Grant roles to a user.
 
    Arguments ``username`` and ``user`` are mutually exclusive. You
    can either set one or the other, but not both. if ``username`` and
    ``user`` are not given, the authenticated member will be used.
 
    :param username: Username of the user that will receive the granted roles.
    :type username: string
    :param user: User object that will receive the granted roles.
    :type user: MemberData object
    :param obj: If obj is set then grant roles on this context. If obj is not
        given, the site root will be used.
    :type obj: content object
    :param roles: List of roles to grant
    :type roles: list of strings
    :raises:
        InvalidParameterError
        MissingParameterError
    :Example: :ref:`user_grant_roles_example`
    """
    if user is None:
        user = get(username=username)
 
    if isinstance(roles, tuple):
        roles = list(roles)
 
    # These roles cannot be granted
    if 'Anonymous' in roles or 'Authenticated' in roles:
        raise InvalidParameterError
 
    roles.extend(get_roles(user=user, obj=obj, inherit=False))
 
    if obj is None:
        user.setSecurityProfile(roles=roles)
    else:
        obj.manage_setLocalRoles(user.getId(), roles)
 
 
@required_parameters('roles')
@mutually_exclusive_parameters('username', 'user')
def revoke_roles(username=None, user=None, obj=None, roles=None):
    """Revoke roles from a user.
 
    Arguments ``username`` and ``user`` are mutually exclusive. You
    can either set one or the other, but not both. if ``username`` and
    ``user`` are not given, the authenticated member will be used.
 
    :param username: Username of the user that will receive the revoked roles.
    :type username: string
    :param user: User object that will receive the revoked roles.
    :type user: MemberData object
    :param obj: If obj is set then revoke roles on this context. If obj is not
        given, the site root will be used.
    :type obj: content object
    :param roles: List of roles to revoke
    :type roles: list of strings
    :raises:
        InvalidParameterError
    :Example: :ref:`user_revoke_roles_example`
    """
    if user is None:
        user = get(username=username)
 
    if isinstance(roles, tuple):
        roles = list(roles)
 
    if 'Anonymous' in roles or 'Authenticated' in roles:
        raise InvalidParameterError
 
    actual_roles = get_roles(user=user, obj=obj)
    if actual_roles.count('Anonymous'):
        actual_roles.remove('Anonymous')
    if actual_roles.count('Authenticated'):
        actual_roles.remove('Authenticated')
 
    roles = list(set(actual_roles) - set(roles))
 
    if obj is None:
        user.setSecurityProfile(roles=roles)
    else:
        obj.manage_setLocalRoles(user.getId(), roles)