from AccessControl import ClassSecurityInfo
from AccessControl.class_init import InitializeClass
from Acquisition import aq_inner
from Acquisition import aq_parent
from borg.localrole.bbb.interfaces import IGroupAwareWorkspace
from borg.localrole.bbb.interfaces import IWorkspace
from borg.localrole.interfaces import ILocalRoleProvider
from borg.localrole.workspace import clra_cache_key
from borg.localrole.workspace import store_on_request
from borg.localrole.workspace import WorkspaceLocalRoleManager
from logging import getLogger
from plone.memoize.view import memoize_contextless
from plone.memoize.volatile import cache
from ploneintranet.patches.pas_groupmanager import COLLECTIVE_WORKSPACE_GROUPID_PATTERN
from ploneintranet.workspace.utils import parent_workspace
from Products.PageTemplates.PageTemplateFile import PageTemplateFile
from Products.PlonePAS.interfaces.plugins import ILocalRolesPlugin
from Products.PluggableAuthService.utils import classImplements
from zope.browser.interfaces import IBrowserView
from zope.component import getAdapters

logger = getLogger(__name__)

manage_addPILocalRoleManagerForm = PageTemplateFile(
    "www/PILocalRoleManagerForm.pt", globals(), __name__="manage_addPIRoleManagerForm"
)


def manage_addPILocalRoleManager(dispatcher, id, title=None, REQUEST=None):
    """Add a PILocalRoleManager to a Pluggable Authentication Services."""
    wlrm = PILocalRoleManager(id, title)
    dispatcher._setObject(wlrm.getId(), wlrm)

    if REQUEST is not None:
        REQUEST.RESPONSE.redirect(
            "{}/manage_workspace?"
            "manage_tabs_message=PILocalRoleManager+added.".format(
                dispatcher.absolute_url()
            )
        )


class PILocalRoleManager(WorkspaceLocalRoleManager):

    meta_type = "PI Local Roles Manager"
    security = ClassSecurityInfo()

    def _getAdapters(self, obj):
        adapters = getAdapters((obj,), ILocalRoleProvider)
        # this is sequence of tuples of the form (name, adapter),
        # we don't really care about the names
        return (a[1] for a in adapters)

    def _parent_chain(self, obj):
        """Iterate over the containment chain, stopping if we hit a
        local role blocker"""
        while obj is not None:
            yield obj
            if getattr(obj, "__ac_local_roles_block__", None):
                return
            new = aq_parent(aq_inner(obj))
            # if the obj is a method we get the class
            obj = getattr(obj, "__self__", new)

    @memoize_contextless
    def parent_workspace(self, obj):
        return parent_workspace(aq_inner(obj))

    def _get_principal_ids(self, user, obj=None):
        """Returns a list of the ids of all involved security
        principals: the user and all groups that they belong
        to."""
        principal_ids = [user.getId()]
        if IBrowserView.providedBy(obj):
            # TODO: bring this upstream or check why it's necessary for 5.2
            obj = obj.context
        elif hasattr(obj, "__func__"):
            try:
                obj = getattr(obj.__self__, "context", obj.__self__)
            except Exception:
                logger.warning("Cannot resolve context for: %r", obj)
                principal_ids.extend(user.getGroups())
                return principal_ids

        workspace = self.parent_workspace(obj)
        workspace_uid = workspace.UID() if workspace else None
        for gid in user.getGroups():
            # If this is not a workspace group append it
            if not COLLECTIVE_WORKSPACE_GROUPID_PATTERN.match(gid):
                principal_ids.append(gid)
            elif workspace_uid:
                # Otherwise check if it is relevant to this workspace
                if workspace_uid in gid:
                    principal_ids.append(gid)
        return principal_ids

    @security.private
    def getRolesInContext(self, user, object):
        # we combine the permission of the user with those of the
        # groups she belongs to
        uf = self._get_userfolder(user)
        if uf is not None:
            # rewrap user with an unwrapped user folder, so
            # _check_context works appropriately
            user = aq_inner(user)
            user = user.__of__(uf)
        principal_ids = self._get_principal_ids(user, object)
        roles = set()
        for obj in self._parent_chain(object):
            if user._check_context(obj):
                count = -1
                for count, a in enumerate(self._getAdapters(obj)):
                    for pid in principal_ids:
                        roles.update(a.getRoles(pid))
                # XXX: BBB code, kicks in only if there's no proper adapter
                if count == -1:
                    workspace = IGroupAwareWorkspace(obj, IWorkspace(obj, None))
                    if workspace is not None:
                        roles.update(workspace.getLocalRolesForPrincipal(user))
                        for group in self._groups(obj, user, workspace):
                            roles.update(workspace.getLocalRolesForPrincipal(group))
        return list(roles)

    @security.private
    @cache(get_key=clra_cache_key, get_cache=store_on_request)
    def checkLocalRolesAllowed(self, user, object, object_roles):
        """Checks if the user has one of the specified roles in the
        given context, short circuits when the first provider granting
        one of the roles is found."""
        uf = self._get_userfolder(user)
        if uf is not None:
            # rewrap user with an unwrapped user folder, so
            # _check_context works appropriately
            user = aq_inner(user)
            user = user.__of__(uf)
        check_roles = set(object_roles)
        principal_ids = self._get_principal_ids(user, object)
        for obj in self._parent_chain(object):
            count = -1
            for count, a in enumerate(self._getAdapters(obj)):
                for pid in principal_ids:
                    roles = a.getRoles(pid)
                    if check_roles.intersection(roles):
                        if user._check_context(obj):
                            return 1
                        else:
                            return 0
            # XXX: BBB code, kicks in only if there's no proper adapter
            if count == -1:
                workspace = IGroupAwareWorkspace(obj, IWorkspace(obj, None))
                if workspace is not None:
                    roles = workspace.getLocalRolesForPrincipal(user)
                    if check_roles.intersection(roles):
                        if user._check_context(obj):
                            return 1
                        else:
                            return 0
                    for group in self._groups(obj, user, workspace):
                        roles = workspace.getLocalRolesForPrincipal(group)
                        if check_roles.intersection(roles):
                            if user._check_context(obj):
                                return 1
                            else:
                                return 0

        return None


classImplements(PILocalRoleManager, ILocalRolesPlugin)
InitializeClass(PILocalRoleManager)
