from plone import api as plone_api
from plone.api.exc import InvalidParameterError
from plone.api.validation import at_least_one_of
from plone.i18n.normalizer import idnormalizer
from ploneintranet.userprofile.content.workgroup import IWorkGroup
from ploneintranet.userprofile.interfaces import IMemberGroup
from z3c.form.interfaces import IValidator
from zope.component import getMultiAdapter

import logging

logger = logging.getLogger(__name__)


def get_groups(context=None, full_objects=True, **kwargs):
    """
    List groups from catalog, avoiding expensive LDAP lookups.

    :param context: Any content object that will be used to find the
        GroupResolver context
    :type context: Content object
    :param full_objects: A switch to indicate if full objects or brains should
        be returned
    :type full_objects: boolean
    :returns: group brains or group objects
    :rtype: iterator
    """
    try:
        mtool = plone_api.portal.get_tool("membrane_tool")
    except InvalidParameterError:
        return []
    if context:
        acl_users = plone_api.portal.get_tool("acl_users")
        try:
            # adapters provided by pi.userprofile and pi.workspace
            members = {x for x in IMemberGroup(context).members}
            # In case of groups, resolve the group members
            for id in list(members):
                group = acl_users.getGroupById(id)
                if group:
                    members.remove(id)
                    # these are not membrane profiles but acl members
                    members = members.union(
                        {user.getId() for user in group.getGroupMembers()}
                    )
            # both context and query: calculate intersection
            if "exact_getGroupId" in kwargs:
                _combi = list(members.intersection(set(kwargs["exact_getGroupId"])))
                kwargs["exact_getGroupId"] = _combi
            else:
                kwargs["exact_getGroupId"] = list(members)
        except TypeError:
            # could not adapt to IMemberGroup
            pass
    portal_type = ("ploneintranet.userprofile.workgroup",)
    search_results = mtool.searchResults(portal_type=portal_type, **kwargs)
    if full_objects:
        return (x.getObject() for x in search_results)
    else:
        return search_results


def get_group_suggestions(context=None, full_objects=True, min_matches=5, **kwargs):
    """
    This is a wrapper around get_groups with the intent of providing
    staggered suggestion of users for a group picker:
    1. Groups from the current context (workspace)
       If not enough groups, add:
       If not enough combined users from 1+2, fallback to:
    2. All groups in the portal.

    List groups from catalog, avoiding expensive LDAP lookups.

    :param context: Any content object that will be used to find the
        GroupResolver context
    :type context: Content object
    :param full_objects: A switch to indicate if full objects or brains should
        be returned
    :type full_objects: boolean
    :param min_matches: Keeps expanding search until this threshold is reached
    :type min_matches: int
    :returns: user brains or user objects
    :rtype: iterator
    """

    def expand(search_results, full_objects):
        """Helper function to delay full object expansion"""
        if full_objects:
            return (x.getObject() for x in search_results)
        else:
            return search_results

    # stage 1 context groups
    if context:
        context_groups = [x for x in get_groups(context, False, **kwargs)]
        if len(context_groups) >= min_matches:
            return expand(context_groups, full_objects)
    # prepare stage 2 and 3
    all_groups = [x for x in get_groups(None, False, **kwargs)]
    # skip stage 2 if not enough users
    if len(all_groups) < min_matches:
        return expand(all_groups, full_objects)
    # fallback to stage 2 all groups
    return expand(all_groups, full_objects)


# def get_users_from_userids_and_groupids(ids=None):
#     """
#     Given a list of userids and groupids return the set of users

#     FIXME this has to be folded into get_users once all groups
#     are represented as workspaces.
#     """
#     acl_users = plone_api.portal.get_tool('acl_users')
#     users = {}
#     for id in ids:
#         group = acl_users.getGroupById(id)
#         if group:
#             for user in group.getGroupMembers():
#                 user_ob = acl_users.getUserById(user.getId())
#                 users[user_ob.getProperty('email')] = user_ob
#         else:
#             user_ob = acl_users.getUserById(id)
#             if user_ob:
#                 users[user_ob.getProperty('email')] = user_ob
#     return users.values()


def get(groupid):
    """Get a Plone Intranet group by id

    :param groupid: ID of the user group to be found
    :type groupid: string
    :returns: Group matching the given groupid
    :rtype: `ploneintranet.userprofile.content.group.MembraneGroup` object
    """
    # try first of all to get the group from the workgroups folder
    portal = plone_api.portal.get()
    group = portal.unrestrictedTraverse(f"groups/{groupid}", None)
    if group is not None:
        return group

    # If we can't find the group there let's ask the membrane catalog
    # and return the first result
    for profile in get_groups(exact_getGroupId=groupid):
        return profile
    # If we cannot find any match we will give up and return None
    return None


@at_least_one_of("groupid", "title")
def create(
    groupid="",
    canonical="",
    title="",
    description="",
    email=None,
    members=None,
    properties=None,
):
    """Create a Plone Intranet group.

    :param email: Email for the new group.
    :type email: string
    :param groupid: the object id for the new group.
    :type groupid: string
    :param canonical: he canonical group name (id)
    :type canonical: string
    :param title: new groups title
    :type title: string
    :param title: new groups description
    :type title: description
    :param properties: Group properties to assign to the new group.
    :type properties: dict
    :returns: Newly created group
    :rtype: `ploneintranet.userprofile.content.group.MembraneGroup` object
    """
    portal = plone_api.portal.get()
    if not groupid:
        groupid = idnormalizer.normalize(title)

    if not canonical:
        canonical = groupid

    norm_id = idnormalizer.normalize(groupid).lstrip("_")
    try:
        groups_container = portal.groups
    except AttributeError:
        logger.error("Required portal.groups is missing")
        return

    if norm_id in groups_container:
        return groups_container[norm_id]

    # We have to manually validate the username
    validator = getMultiAdapter(
        (portal, None, None, IWorkGroup["id"], None), IValidator
    )
    validator.validate(groupid)

    profile = plone_api.content.create(
        container=groups_container,
        type="ploneintranet.userprofile.workgroup",
        id=norm_id,
        canonical=canonical,
        title=title or groupid,
        description=description,
        email=email,
        members=members or set(),
        **properties or {},
    )

    return profile
