# coding: utf-8
from typing import Tuple, Dict, Any

from attr import attr, attributes, asdict
from attr.validators import instance_of, optional

from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.utils.translation import ugettext_lazy as _

from idm.core.workflow.exceptions import SilentReferenceRolesValidationError, NotifyReferenceRolesValidationError
from idm.nodes.canonical import tuple_of, dict_of, Hashable, SELF
from idm.users.models import User

from idm.core.workflow.exceptions import RoleNodeDoesNotExist


@attributes(slots=True)
class CanonicalField:
    slug: str = attr(validator=instance_of(str))
    type: str = attr(validator=instance_of(str))
    is_required: bool = attr(validator=instance_of(bool))
    name: str = attr(validator=instance_of(str))
    name_en: str = attr(validator=instance_of(str))
    options: Dict[str, Any] = attr(validator=optional(instance_of(dict)))
    dependencies: Dict[str, Any] = attr(validator=optional(instance_of(dict)))

    def as_key(self):
        return self.type, self.slug

    def as_dict(self):
        return asdict(self)


@attributes(slots=True)
class CanonicalAlias:
    type: str = attr(validator=instance_of(str), default='default')
    name: str = attr(validator=instance_of(str), default='')
    name_en: str = attr(validator=instance_of(str), default='')

    def as_key(self):
        return self.type, self.name, self.name_en


@attributes(slots=True)
class CanonicalResponsibility:
    user: User = attr(validator=instance_of(User))
    notify: bool = attr(validator=instance_of(bool), default=False)

    def as_key(self):
        return self.user.username


@attributes(slots=True)
class CanonicalNode(Hashable):
    # атрибуты со значением по умолчанию
    slug: str = attr(validator=instance_of(str))
    name: str = attr(validator=instance_of(str), default='')
    name_en: str = attr(validator=instance_of(str), default='')
    description: str = attr(validator=instance_of(str), default='')
    description_en: str = attr(validator=instance_of(str), default='')
    is_exclusive: bool = attr(validator=instance_of(bool), default=False)
    is_public: bool = attr(validator=instance_of(bool), default=True)

    unique_id: str = attr(validator=instance_of(str), default='')
    review_required = attr(validator=optional(instance_of(bool)), default=None)
    comment_required = attr(validator=optional(instance_of(bool)), default=False)
    set: str = attr(validator=instance_of(str), default='')

    # вложенные сущности
    fields: Dict[str, CanonicalField] = attr(validator=dict_of(CanonicalField), factory=dict)
    aliases: Dict[str, CanonicalAlias] = attr(validator=dict_of(CanonicalAlias), factory=dict)
    responsibilities: Dict[str, CanonicalResponsibility] = \
        attr(validator=dict_of(CanonicalResponsibility), factory=dict)

    # дочерние объекты
    children: Tuple['CanonicalNode'] = \
        attr(validator=tuple_of(SELF), cmp=False, repr=False, hash=False, default=(), converter=tuple)


@attributes(slots=True)
class CanonicalRef:
    system = attr()
    node = attr()
    fields_data = attr()
    is_public = attr(validator=optional(instance_of(bool)))

    @classmethod
    def from_dict(cls, ref):
        from idm.core.models import System, RoleNode

        system = None
        node = None
        try:
            system = System.objects.select_related('actual_workflow').get(slug=ref.get('system', '').lower())
            node = RoleNode.objects.get_node_by_data(system, ref.get('role_data'), select_related=['system'])
        except (ObjectDoesNotExist, MultipleObjectsReturned, RoleNodeDoesNotExist):
            pass

        fields = ref.get('role_fields', {})
        if fields and node:
            actual_fields = {field.slug for field in node.get_fields()}

            if actual_fields.issuperset(fields.keys()):
                fields = dict(sorted(fields.items()))
            else:
                fields = None

        return cls(
            system=system,
            node=node,
            fields_data=fields,
            is_public=ref.get('visibility'),
        )

    @classmethod
    def from_model(cls, ref):
        if ref.fields_data:
            fields_data = ref.fields_data.copy()
        else:
            fields_data = {}

        return cls(
            system=ref.system,
            node=ref.node,
            fields_data=fields_data,
            is_public=ref.is_public,
        )

    def validate(self):
        if self.system is None:
            raise NotifyReferenceRolesValidationError(_('Системы не существует'))

        elif self.node is None:
            raise SilentReferenceRolesValidationError(_('Узла не существует'))

        elif self.fields_data is None:
            raise NotifyReferenceRolesValidationError(_('Указаны несуществующие fields'))


@attributes(slots=True)
class Decision:
    proposal_id = attr()
    applied_at = attr()
    changing_duties = attr()

    @classmethod
    def from_dict(cls, **kwargs):
        allowed_keys = ['proposal_id', 'applied_at', 'changing_duties']
        kwargs = {key: value for key, value in kwargs.items() if key in allowed_keys}
        kwargs.setdefault('proposal_id', None)
        return cls(**kwargs)

    def as_dict(self):
        return {
            'proposal_id': self.proposal_id,
            'applied_at': self.applied_at,
            'changing_duties': self.changing_duties,
        }


@attributes(slots=True)
class MultiDecision:
    decisions = attr(validator=instance_of(list))

    @classmethod
    def from_list(cls, list_):
        decisions = [Decision.from_dict(**item) for item in list_]
        instance = cls(decisions=decisions)
        return instance

    @property
    def result(self):
        if len(self.decisions) == 0:
            return None
        if any(decision.changing_duties for decision in self.decisions):
            return True
        if any(decision.changing_duties is False for decision in self.decisions):
            return False
        return None

    def as_data(self):
        data = {
            'decisions': [
                decision.as_dict() for decision in self.decisions
            ]
        }
        return data


class UserMultiDecision(MultiDecision):
    @property
    def result(self):
        return len(self.decisions) > 0
