from BTrees.OOBTree import OOBTree
from BTrees.OOBTree import OOTreeSet
from functools import reduce
from logging import getLogger
from networkx import DiGraph
from plone import api
from plone.memoize.view import memoize_contextless
from Products.Five import BrowserView
from zope.annotation import IAnnotations

logger = getLogger(__name__)


class View(BrowserView):
    _key2type = {
        "pas_uid2path": OOBTree,
        "pas_path2name": OOBTree,
        "pas_name2uid": OOBTree,
        "pas_edges": OOTreeSet,
    }

    @property
    @memoize_contextless
    def _storage(self):
        return IAnnotations(api.portal.get())

    def get_or_create_btree(self, key):
        storage = self._storage
        if key not in storage:
            storage[key] = self._key2type[key]()
        return storage[key]

    @property
    def uid2path(self):
        return self.get_or_create_btree("pas_uid2path")

    @property
    def path2name(self):
        return self.get_or_create_btree("pas_path2name")

    @property
    def name2uid(self):
        return self.get_or_create_btree("pas_name2uid")

    @property
    def edges(self):
        return self.get_or_create_btree("pas_edges")

    def get_pas_graph_for(self, obj):
        return api.content.get_view("pas-graph", obj, self.request)

    def _clear_cache(self):
        # XXX Break cache in a better way
        IAnnotations(self.request, {}).get("plone.memoize", {}).clear()

    def add(self, obj):
        view = self.get_pas_graph_for(obj)
        for uid, name, path in view.get_node_info():
            self.uid2path[uid] = path
            self.path2name[path] = name
            self.name2uid[name] = uid
            if uid.partition(":") not in self.principalname_prefixes:
                self._clear_cache()
            logger.info("Added node %r", uid)

        self.edges.update(view.get_edges())

    def remove(self, obj):
        view = self.get_pas_graph_for(obj)
        uids = set()
        for uid, name, path in view.get_node_info():
            uids.add(uid)
            self.uid2path.pop(uid, None)
            self.path2name.pop(path, None)
            self.name2uid.pop(name, None)

        if not uids:
            return
        logger.info("Removed nodes %r", uids)
        edges_to_remove = set()
        for edge in self.edges:
            if uids.intersection(edge):
                edges_to_remove.add(edge)
        logger.info("Removed edges %r", edges_to_remove)
        list(map(self.edges.remove, edges_to_remove))

    def update(self, obj):
        self._clear_cache()
        view = self.get_pas_graph_for(obj)
        uids = set()
        for uid, name, path in view.get_node_info():
            uids.add(uid)
            self.uid2path[uid] = path
            self.path2name[path] = name
            self.name2uid[name] = uid

        current_edges = {edge for edge in self.edges if edge[0] in uids}
        new_edges = view.get_edges()
        if current_edges == new_edges:
            return
        edges_to_remove = current_edges - new_edges
        edges_to_add = new_edges - current_edges
        list(map(self.edges.remove, edges_to_remove))
        list(map(self.edges.add, edges_to_add))
        logger.info("Removed edges %r", edges_to_remove)
        logger.info("Added edges %r", edges_to_add)

    @property
    @memoize_contextless
    def graph(self):
        graph = DiGraph()
        graph.add_nodes_from(self.uid2path)
        graph.add_edges_from(self.edges)
        return graph

    def predecessors(self, uid):
        return set(self.graph.predecessors(uid))

    def predecessors_for_uids(self, uids):
        if not uids:
            return set()
        return reduce(set.union, list(map(self.predecessors, uids)))

    @memoize_contextless
    def ancestors(self, uid):
        ancestors = self.predecessors(uid)
        new_uids_sets = self.predecessors_for_uids(ancestors) - ancestors
        ancestors.update(new_uids_sets)
        while new_uids_sets:
            new_uids_sets = self.predecessors_for_uids(new_uids_sets) - ancestors
            ancestors.update(new_uids_sets)
        return ancestors

    def successors(self, uid):
        return set(self.graph.successors(uid))

    def successors_for_uids(self, uids):
        if not uids:
            return set()
        return reduce(set.union, list(map(self.successors, uids)))

    @memoize_contextless
    def descendants(self, uid):
        descendants = self.successors(uid)
        new_uids_sets = self.successors_for_uids(descendants) - descendants
        descendants.update(new_uids_sets)
        while new_uids_sets:
            new_uids_sets = self.successors_for_uids(new_uids_sets) - descendants
            descendants.update(new_uids_sets)
        return descendants

    @property
    @memoize_contextless
    def principalname_prefixes(self):
        return {name.partition(":")[0] for name in self.name2uid}

    def resolvepasuid(self, uid):
        path = self.uid2path[uid]
        return self.context.restrictedTraverse(path.rpartition(":")[-1])

    @memoize_contextless
    def principalname2uid(self, principalname):
        """principalname should be a userid.
        See #1054 for background."""
        principalname = principalname
        for prefix in self.principalname_prefixes:
            name = f"{prefix}:{principalname}"
            if name in self.name2uid:
                return self.name2uid[name]
        logger.warning("Cannot resolve uid for %r", principalname)
        return ""

    def uids2principalnames(self, uids):
        uid2path = self.uid2path
        path2name = self.path2name
        paths = (uid2path.get(uid) for uid in uids)
        names = (path2name.get(path) for path in paths)
        return {name.partition(":")[2] for name in names if name}

    @memoize_contextless
    def _get_recursive_group_names(self, principalname):
        """principalname should be a string type"""
        try:
            uid = self.principalname2uid(principalname)
        except Exception:
            logger.exception("Cannot resolve uid for %r", principalname)
            uid = None
        if not uid:
            return ()
        ancestors_uids = self.ancestors(uid)
        return tuple(self.uids2principalnames(ancestors_uids))

    def get_recursive_group_names(self, principal):
        """user is a MemberData object"""
        if isinstance(principal, str):
            principalname = principal
        else:
            principalname = principal.getId()
        return self._get_recursive_group_names(principalname)

    def __call__(self):
        return self
