"""specific hooks for Classification and Keyword entities

:organization: Logilab
:copyright: 2007-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
"""

from itertools import chain

from cubicweb import ValidationError
from cubicweb.server.hook import Hook, Operation, match_rtype
from yams.schema import role_name
from cubicweb import _


class BeforeAddDescendantOf(Hook):
    """check indirect cycle for ``descendant_of`` relation"""

    __regid__ = "beforeadddescendant"
    events = ("before_add_relation",)
    __select__ = Hook.__select__ & match_rtype("descendant_of")

    def __call__(self):
        entity = self._cw.entity_from_eid(self.eidfrom)
        parent = self._cw.entity_from_eid(self.eidto)
        parents = {
            x.eid
            for x in chain(
                [
                    parent,
                ],
                parent.cw_adapt_to("ITree").iterparents(),
            )
        }
        children = {
            x.eid
            for x in chain([entity], entity.cw_adapt_to("ITree").recurse_children())
        }
        if children & parents:
            msg = _("detected descendant_of cycle")
            raise ValidationError(self.eidfrom, {role_name(self.rtype, "subject"): msg})


class AfterAddSubKeywordOf(Hook):
    """sets ``descendant_of`` relation"""

    __regid__ = "afteradddescendant"
    events = ("after_add_relation",)
    __select__ = Hook.__select__ & match_rtype("subkeyword_of")

    def __call__(self):
        entity = self._cw.entity_from_eid(self.eidfrom)
        parent = self._cw.entity_from_eid(self.eidto)
        SetDescendantOfKeywordOp(self._cw, parent=parent, entity=entity)


class SetIncludedInRelationHook(Hook):
    """sets the included_in relation on a subkeyword if not already set"""

    __regid__ = "setincludedinrelationhook"
    events = ("before_add_relation",)
    __select__ = Hook.__select__ & match_rtype("subkeyword_of")

    def __call__(self):
        # immediate test direct cycles
        if self.eidfrom == self.eidto:
            msg = self._cw._("keyword cannot be subkeyword of himself")
            raise ValidationError(self.eidfrom, {role_name(self.rtype, "subject"): msg})
        SetIncludedInRelationOp(
            self._cw, vreg=self._cw.vreg, eidfrom=self.eidfrom, eidto=self.eidto
        )


class RemoveDescendantOfRelation(Hook):
    """removes ``descendant_of`` relation

    we delete the relation for entity's parents recursively
    """

    __regid__ = "removedescendantofrelation"
    events = ("after_delete_relation",)
    __select__ = Hook.__select__ & match_rtype("subkeyword_of")

    def __call__(self):
        parent = self._cw.entity_from_eid(self.eidto)
        for parent in chain([parent], parent.cw_adapt_to("ITree").iterparents()):
            self._cw.execute(
                "DELETE K descendant_of P WHERE K eid %(k)s, " "P eid %(p)s",
                {"p": parent.eid, "k": self.eidfrom},
            )


# operations #################################################################
class SetIncludedInRelationOp(Operation):
    """delay this operation to commit to avoid conflict with a late rql query
    already setting the relation
    """

    def precommit_event(self):
        cnx = self.cnx
        # test for indirect cycles
        self.check_cycle()
        subkw = cnx.entity_from_eid(self.eidfrom)
        if subkw.included_in:
            kw = cnx.entity_from_eid(self.eidto)
            if subkw.included_in[0].eid != kw.included_in[0].eid:
                msgid = (
                    "keywords %(subkw)s and %(kw)s belong to different classifications"
                )
                raise ValidationError(
                    subkw.eid,
                    {
                        role_name("subkeyword_of", "subject"): cnx._(msgid)
                        % {"subkw": subkw.eid, "kw": kw.eid}
                    },
                )
        else:
            cnx.execute(
                "SET SK included_in C WHERE SK eid %(x)s, "
                "SK subkeyword_of K, K included_in C",
                {"x": subkw.eid},
            )

    def check_cycle(self):
        parents = {self.eidto}
        parent = self.cnx.entity_from_eid(self.eidto)
        while parent.subkeyword_of:
            parent = parent.subkeyword_of[0]
            if parent.eid in parents:
                msg = self.cnx._("detected subkeyword cycle")
                raise ValidationError(
                    self.eidfrom, {role_name("subkeyword_of", "subject"): msg}
                )
            parents.add(parent.eid)


class SetDescendantOfKeywordOp(Operation):
    def precommit_event(self):
        """transitive closure of ``descendant_of`` relations to current entity"""
        closure = set()
        entity = self.entity
        parent = self.parent
        for parent in chain(
            [parent, entity], parent.cw_adapt_to("ITree").iterparents()
        ):
            for child in chain(
                [entity], entity.cw_adapt_to("ITree").recurse_children()
            ):
                if child.eid != parent.eid:
                    closure.add((child, parent))
        for child, parent in closure:
            descendants = [p.eid for p in child.descendant_of]
            if parent.eid not in descendants:
                child.cw_set(descendant_of=parent)
