from AccessControl import ClassSecurityInfo
from plone import api
from plone.app.layout.navigation.interfaces import INavigationRoot
from plone.app.uuid.utils import uuidToObject
from plone.memoize.instance import memoize
from ploneintranet.userprofile.browser.userprofile import default_avatar
from ploneintranet.userprofile.browser.userprofile import stream_avatar_data
from ploneintranet.userprofile.content.userprofile import IUserProfile
from ploneintranet.userprofile.interfaces import IMemberGroup
from Products.CMFCore.interfaces import ISiteRoot
from slc.mailrouter.interfaces import IEmailToUser
from zope.annotation.interfaces import IAnnotations
from zope.component import adapter
from zope.globalrequest import getRequest
from zope.interface import implementer
from zope.publisher.interfaces.browser import IBrowserRequest
from zope.traversing.namespace import SimpleHandler


def search_membergroup(context):
    """
    This deceptively simple algorithm relies on the ZCA
    'more specific interface' preference.

    This is the fallback adapter factory that only gets called
    if no more specific adapter for the context is registered.

    It simply delegates to the aq_parent. If that does have a
    more specific adapter, the recursion breaks. Else this factory
    is hit again and walks the acquisition tree upward, until
    it is pre-empted by a more specific adapter or reaches ISiteRoot.

    The goal is to take a content object, and walk the acquisition tree
    upward until the specific workspace adapter is found for e.g.
    ploneintranet.workspace.workspacefolder.IWorkspaceFolder which
    is registered in ploneintranet.workspace.

    This scheme makes it possible to later registere additional specific
    adapters for e.g. library sections etc.
    """
    if ISiteRoot.providedBy(context):
        return  # raises TypeError 'Could not adapt'
    # recurse upward until a more specific adapter takes precedence
    return IMemberGroup(context.aq_inner.aq_parent)


class MembraneGroupsProvider:
    """
    Determine the groups to which a principal belongs.
    A principal can be a user or a group.

    This is a plugin provider, used by Products.membrane. When the groups
    of a user are determined, this is roughly the call flow:

    The main method that plays a role is
    `Products.PluggableAuthService.PluggableAuthService.PluggableAuthService.
        _getGroupsForPrincipal`.
    This method iterates over all plugins that are registered as groupmakers
    (= they implement `IGroupsPlugin`). Among those plugins are source_groups
    and auto_group, and also our workspace_groups plugin from
    `collective.workspace.pas.WorkspaceGroupManager`.

    But in this case, the membrane_groups plugin is the one we are interested
    in. In `Products.membrane.plugins.groupmanager.getGroupsForPrincipal`
    the providers that can be used for looking up the group memberships of the
    principal are found via a call to `findMembraneUserAspect` of
    Products.membrane.utils.
    It does a catalog query for all objects that implement
    `user_ifaces.IMembraneUserGroups` and fulfill the query (exact match of the
    user id). Then it applies this interface to the results and returns them.
    This is why this provider needs to announce that it provides the interface
    `Products.membrane.interfaces.user.IMembraneUserGroups`
    for membrane user objects `dexterity.membrane.behavior.user.IMembraneUser`.
    Otherwise this provider will not be found by the membrane_plugin when it
    is handling a user.

    Similar for groups: we register this adapter for the interface
    `dexterity.membrane.behavior.group.IMembraneGroup` to
    `Products.membrane.interfaces.group.IMembraneGroupGroups`,
    which is again called by
    `Products.membrane.plugins.groupmanager.getGroupsForPrincipal`.

    It is a bit quirky. The core membrane function
    `Products.membrane.plugins.groupmanager.getGroupsForPrincipal`
    gathers a list of providers.  The only providers it should be able
    to find, are one principal: a user or group with the asked id.
    Or rather: an adapter of this user or group.  In our case this will be
    an instance of the MembraneWorkspaceGroupsProvider class.  Then it asks
    this adapter for the list of groups of a principal.  But this
    principal is this same user or group from the adapter.
    So the groupmanager could simply ask: give me your groups.

    We will look at the passed principal anyway, which also avoids the
    need to determine whether we need to call getUserId or getGroupId
    or getId to get the id of this user.

    See quaive/ploneintranet#415 for a discussion of this.
    """

    security = ClassSecurityInfo()

    _cache_key = "ploneintranet_group_member_cache"

    def __init__(self, context):
        self.context = context

    @memoize
    def get_cache(self, request=None):
        request = request or getRequest() or {}
        annotations = IAnnotations(request, {})
        return annotations.get(self._cache_key, {})

    def clear_cache(self, principalids=[], request=None):
        cache = self.get_cache(request)
        if not cache:
            return
        if not principalids:
            cache.clear()
            return
        for principalid in principalids:
            cache.pop(principalid, None)

    def _getGroupsForPrincipalId(self, principalid, request=None):
        if not principalid:
            return ()

        mt = api.portal.get_tool("membrane_tool")
        query = {"workspace_members": principalid}
        return tuple(
            b.getGroupId for b in mt.unrestrictedSearchResults(query) if b.getGroupId
        )

    # IGroupsPlugin implementation
    @security.private
    def getGroupsForPrincipal(self, principal, request=None):
        cache = self.get_cache(request)
        principalid = principal.getId()
        if principalid not in cache:
            cache[principalid] = self._getGroupsForPrincipalId(principalid, request)
        return cache[principalid]


@implementer(IEmailToUser)
@adapter(ISiteRoot)
class Email2User:
    order = 100

    def __init__(self, context):
        self.context = context

    def __call__(self, email):
        """Resolve the user looking for him in the membrane catalog"""
        if not email:
            return
        mt = api.portal.get_tool("membrane_tool")
        results = mt.unrestrictedSearchResults(
            email=email, object_provides=IUserProfile.__identifier__
        )
        for result in results:
            member = api.user.get(username=result.getUserName)
            if member:
                return member.getUser()


@adapter(INavigationRoot, IBrowserRequest)
class ProfileAvatarTraverser(SimpleHandler):
    """Publicly accessible profile icon view traverser for notifications."""

    def traverse(self, id, ignored=None):
        uuid = id.split(".")[0]
        try:
            profile = uuidToObject(uuid, unrestricted=True)
        except TypeError:
            # p.a.uuid < 2.2.0 does not support unrestricted
            pc = api.portal.get_tool("portal_catalog")
            brains = pc.unrestrictedSearchResults(UID=uuid)
            try:
                profile = brains[0]._unrestrictedGetObject()
            except IndexError:
                profile = None
        enabled = api.content.get_state(obj=profile) == "enabled" if profile else False
        if enabled:
            return stream_avatar_data(profile, "stream", getRequest())
        return default_avatar(getRequest().response)
