# coding: utf-8


import attr
import logging

from django.conf import settings
from django.db.models import Count
from django.utils import timezone

from idm.core import signals
from idm.core.constants.action import ACTION
from idm.core.constants.instrasearch import INTRASEARCH_METHOD
from idm.core.constants.node_relocation import RELOCATION_STATE
from idm.nodes.queue import Queue as BaseQueue, AdditionItem, ModificationItem, RemovalItem, RestoringItem


log = logging.getLogger(__name__)


@attr.s(slots=True)
class NodeAdditionItem(AdditionItem):
    def apply(self, **kwargs):
        from idm.core.models import RoleNode, RoleNodeSet

        log.info('Apply add node %s', self.new_node)
        parent = self.get_parent()

        requester = kwargs.get('user')
        sync_key = kwargs.get('sync_key')
        from_api = kwargs.get('from_api', False)

        parent.fetch_system()
        self.new_node = RoleNode.from_canonical(self.child_data, parent.system)
        self.new_node.parent = parent
        self.new_node.is_key = not parent.is_key

        action_data = {}

        moved_from = None
        if self.child_data.unique_id:
            moved_from = RoleNode.objects.get_node_by_unique_id(parent.system, self.child_data.unique_id,
                                                                include_depriving=True)
            if moved_from is not None:
                self.new_node.moved_from = moved_from
                action_data['moved_from'] = {
                    'id': moved_from.pk,
                    'path': moved_from.path
                }
        self.new_node.hash = ''

        was_renamed = False
        should_update_set = True

        # Теперь это обычный save, при конфликте падаем
        self.new_node.relocation_state = RELOCATION_STATE.SUPERDIRTY
        self.new_node.save()

        self.new_node = RoleNode.objects.select_related('system').prefetch_related('responsibilities').get(pk=self.new_node.pk)

        # этот узел станет depriving в процессе синхронизации. Форсируем его удаление
        if moved_from is not None and (not moved_from.depriving_at or moved_from.depriving_at > timezone.now()):
            moved_from.depriving_at = timezone.now()
            moved_from.save(update_fields=['depriving_at'])

        diff = self.get_diff(cmp_attrs=['set', 'fields', 'aliases', 'responsibilities'])
        assert len(diff.flat) <= 1, 'There can be only "set" field changed among flat ones'
        # Updating nodeset, fields, aliases, responsibilities
        self.new_node.was_renamed = was_renamed

        log.info('Add aliases to new node %s', self.new_node)
        self.new_node.add_aliases([addition.new for addition in diff.nested['aliases'].additions], requester, sync_key)

        log.info('Remove aliases from new node %s', self.new_node)
        self.new_node.remove_aliases([removal.old for removal in diff.nested['aliases'].removals], requester, sync_key)

        log.info('Add fields to new node %s', self.new_node)
        self.new_node.add_fields([addition.new for addition in diff.nested['fields'].additions], requester, sync_key)
        log.info('Remove fields from new node %s', self.new_node)
        self.new_node.remove_fields([removal.old for removal in diff.nested['fields'].removals], requester, sync_key)
        log.info('Change fields in new node %s', self.new_node)
        self.new_node.change_fields([modification for modification in diff.nested['fields'].modifications], requester,
                                    sync_key)

        log.info('Add responsibilities to new node %s', self.new_node)
        self.new_node.add_responsibilities([addition.new for addition in diff.nested['responsibilities'].additions],
                                           requester, sync_key)

        log.info('Remove responsibilities from new node %s', self.new_node)
        self.new_node.remove_responsibilities([removal.old for removal in diff.nested['responsibilities'].removals],
                                              requester, sync_key)

        log.info('Change fields in new node %s', self.new_node)
        self.new_node.change_responsibilities([modification for modification in
                                               diff.nested['responsibilities'].modifications], requester, sync_key)
        # Creating RoleNodeSet if necessary
        if should_update_set and len(diff.flat) == 1:
            changed_set = diff.flat[0]
            if changed_set.new:
                self.new_node.nodeset = RoleNodeSet.objects.restore_or_create(
                    system=self.new_node.system,
                    set_id=changed_set.new,
                    name=self.new_data.name,
                    name_en=self.new_data.name_en
                )
            else:
                RoleNodeSet.objects.unlink_or_deprive_if_last(node=self.new_node)

        action_data['from_api'] = from_api
        action = self.new_node.actions.create(
            action=ACTION.ROLE_NODE_CREATED,
            system=parent.system,
            data=action_data,
            requester=requester,
            parent=sync_key,
        )
        signals.role_node_added.send(sender=parent, role_node=self.new_node, moved_from=moved_from, action=action)

    def get_parent(self):
        if self.node is not None:
            parent = self.node
        else:
            parent = self.parent_item.new_node
        return parent

    def get_name_list(self):
        if self.node:
            path = [item['name'] for item in self.node.fullname]
        else:
            path = self.parent_item.get_name_list()
        path.append(self.child_data.name)
        return path

    def get_full_name(self):
        name_list = self.get_name_list()
        combined = ''
        length = len(name_list)
        for index, part in enumerate(name_list):
            combined += part
            if index != length -1:
                combined += ', ' if index % 2 else ': '
        return combined


@attr.s(slots=True)
class NodeModificationItem(ModificationItem):
    was_renamed = attr.ib(default=False)

    def apply(self, **kwargs):
        from idm.core.models import RoleNodeSet

        log.info('Apply modification node %s', self.node)
        diff = self.get_diff()
        sync_key = kwargs.get('sync_key')
        requester = kwargs.get('user')
        from_api = kwargs.get('from_api', False)

        update_fields = []
        data_has_changed = bool(diff.flat)
        was_renamed = False
        should_recalc_path = False
        nodeset_renamed = False

        if data_has_changed:
            signals.role_node_modified_before.send(sender=self.node, role_node=self.node, diff=diff)

        for item in diff.flat:
            if item.name in ('name', 'name_en'):
                was_renamed = True
                nodeset_renamed = True
            elif item.name in ('description', 'description_en'):
                was_renamed = True
            elif item.name == 'slug':
                should_recalc_path = True
            if item.name == 'set':
                if item.new:
                    update_fields.append('nodeset')
                    self.node.nodeset = RoleNodeSet.objects.restore_or_create(
                        system=self.node.system,
                        set_id=item.new,
                        name=self.new_data.name,
                        name_en=self.new_data.name_en
                    )
                else:
                    RoleNodeSet.objects.unlink_or_deprive_if_last(node=self.node)
            else:
                update_fields.append(item.name)
                setattr(self.node, item.name, item.new)

        if nodeset_renamed and self.node.nodeset:
            self.node.nodeset.name = self.node.name
            self.node.nodeset.name_en = self.node.name_en
            self.node.nodeset.save()
        self.was_renamed = was_renamed

        log.info('Add aliases to node %s', self.node)
        self.node.add_aliases([addition.new for addition in diff.nested['aliases'].additions], requester, sync_key)

        log.info('Remove aliases from node %s', self.node)
        self.node.remove_aliases([removal.old for removal in diff.nested['aliases'].removals], requester, sync_key)

        log.info('Remove fields from node %s', self.node)
        self.node.remove_fields([removal.old for removal in diff.nested['fields'].removals], requester, sync_key)

        log.info('Add fields to node %s', self.node)
        self.node.add_fields([addition.new for addition in diff.nested['fields'].additions], requester, sync_key)

        log.info('Change fields in node %s', self.node)
        self.node.change_fields([modification for modification in diff.nested['fields'].modifications], requester,
                                sync_key)

        log.info('Add responsibilities to node %s', self.node)
        self.node.add_responsibilities([addition.new for addition in diff.nested['responsibilities'].additions],
                                       requester, sync_key)

        log.info('Remove responsibilities from node %s', self.node)
        self.node.remove_responsibilities([removal.old for removal in diff.nested['responsibilities'].removals],
                                          requester, sync_key)

        log.info('Change responsibilities in node %s', self.node)
        self.node.change_responsibilities([modification for modification in
                                           diff.nested['responsibilities'].modifications], requester, sync_key)

        if was_renamed or should_recalc_path:
            update_fields.append('relocation_state')
            self.node.relocation_state = RELOCATION_STATE.CT_COMPUTED

        if update_fields or self.node.system.slug not in settings.IDM_SYSTEMS_WITHOUT_INFO:
            self.node.hash = ''
            update_fields += ['hash', 'updated_at']
            self.node.save(recalc_fields=(should_recalc_path or was_renamed), update_fields=update_fields)

        if should_recalc_path:
            for child in self.node.get_descendants(include_self=False):
                child.send_intrasearch_push(INTRASEARCH_METHOD.REMOVE)

        if (was_renamed or should_recalc_path) and not from_api:
            for node in self.node.get_descendants(include_self=True).order_by('level'):
                node.save(recalc_fields=True, update_fields=[])

        if data_has_changed:
            action = self.node.actions.create(
                action=ACTION.ROLE_NODE_CHANGED,
                system=self.node.system,
                requester=requester,
                parent=sync_key,
                data={
                    'from_api': from_api,
                    'diff': {item.name: [item.old, item.new] for item in diff.flat},
                }
            )
            signals.role_node_modified.send(sender=self.node, role_node=self.node, diff=diff, action=action)
        return self.node


@attr.s(slots=True)
class NodeRemovalItem(RemovalItem):
    def apply(self, **kwargs):
        log.info('Apply remove node %s', self.node)
        self.node.mark_depriving()


@attr.s(slots=True)
class NodeRestoringItem(RestoringItem):
    def apply(self, **kwargs):
        log.info('Apply restore node %s', self.node)
        self.node.mark_active()


@attr.s(slots=True)
class RoleNodeQueue(BaseQueue):
    addition = NodeAdditionItem
    modification = NodeModificationItem
    removal = NodeRemovalItem
    restore = NodeRestoringItem

    def push_addition(self, child_data, *args, **kwargs):
        if kwargs.get('in_auto_mode'):
            item = super(RoleNodeQueue, self).push_addition(child_data, *args, **kwargs)
            self.push_hash_update(node=None, hash=child_data.hash, parent_item=item)

    def push_modification(self, *args, **kwargs):
        if kwargs.get('in_auto_mode'):
            super(RoleNodeQueue, self).push_modification(*args, **kwargs)

    def push_removal(self, node, extra=None, **kwargs):
        if kwargs.get('in_auto_mode') or node.is_auto_updated:
            super(RoleNodeQueue, self).push_removal(node=node, extra=extra, **kwargs)

    def push_restore(self, *args, **kwargs):
        if kwargs.get('in_auto_mode'):
            super(RoleNodeQueue, self).push_restore(*args, **kwargs)

    # методы для представления очереди во вьюхе system_roles_management
    def as_context(self):
        added_nodes_names = self.get_added_nodes_names()
        removed_nodes_names, roles_count, users_count, groups_count = self.get_removed_nodes_info()
        modifications = self.get_modifications()
        context = {
            'additions': added_nodes_names,
            'additions_count': len(added_nodes_names),
            'removals': removed_nodes_names,
            'removals_count': len(removed_nodes_names),
            'roles_count_to_deprive': roles_count,
            'users_count_to_deprive': users_count,
            'groups_count_to_deprive': groups_count,
            'modifications': modifications,
            'modifications_count': len(modifications),
        }
        return context

    def get_added_nodes_names(self):
        names = [item.get_full_name() for item in self.get_of_type('add')]
        return names

    def get_removed_nodes_info(self):
        from idm.core.models import RoleNode, Role
        from idm.users.models import User, Group
        new_unique_ids = {item.child_data.unique_id for item in self.get_of_type('add') if item.child_data.unique_id}
        nodes = [item.node for item in self.get_of_type('remove')]
        # Роли будут отозваны только у тех узлов, для которых нет AdditionItem с таким же unique_id
        nodes_in_danger = [node for node in nodes if node.unique_id not in new_unique_ids]
        nodes_qs = RoleNode.objects.active().filter(id__in=[node.pk for node in nodes_in_danger])
        annotated = nodes_qs.filter(roles__is_active=True).annotate(roles_count=Count('roles'))
        annotated_dict = dict(annotated.values_list('pk', 'roles_count'))
        roles = Role.objects.filter(is_active=True, node__in=nodes_qs)
        roles_count = roles.count()
        users_count = User.objects.filter(roles__in=roles).order_by('username').distinct().count()
        groups_count = Group.objects.active().filter(roles__in=roles).order_by('external_id').distinct().count()
        removals = []
        for node in nodes:
            removals.append((node.humanize(), annotated_dict.get(node.pk, 0)))
        return removals, roles_count, users_count, groups_count

    def get_modifications(self):
        return self.get_of_type('modify')

    def apply(self, run_node_pipeline=True, **kwargs):
        result = super(RoleNodeQueue, self).apply(**kwargs)
        if run_node_pipeline:
            from idm.core.tasks.nodes import RecalcNodePipelineTask
            RecalcNodePipelineTask.apply_async(kwargs={
                'system_id': self.system.pk,
            })
        return result
