# coding: utf-8


import logging
import random
import re
from collections import defaultdict, Iterable
from hashlib import md5
from itertools import chain
from typing import List, Union

from django.conf import settings
from django.contrib.auth.models import AbstractUser, Permission
from django.db import models, transaction
from django.db.models import Q, QuerySet
from django.dispatch import receiver
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django_pgaas import atomic_retry
from ids.registry import registry

from idm.core.constants.action import ACTION
from idm.core.constants.affiliation import AFFILIATION
from idm.core.constants.email_templates import PASSPORT_LOGIN_NEED_ATTACH_NEW_GROUP_ROLE_TEMPLATES
from idm.core.constants.groupmembership import GROUPMEMBERSHIP_STATE
from idm.core.constants.rolefield import FIELD_TYPE
from idm.core.workflow.exceptions import PassportLoginPolicyError, AccessDenied
from idm.core.exceptions import MultiplePassportLoginsError, SynchronizationError
from idm.framework.fields import StrictForeignKey
from idm.framework.mixins import LocalizedModel, DifferenceTableMixin
from idm.nodes.models import ExternalTrackingNode
from idm.permissions.utils import get_permission
from idm.services.models import Service as GroupService
from idm.users import canonical
from idm.users import ranks as ranks_constants
from idm.users import signals
from idm.users.constants.group import GROUP_TYPES
from idm.users.constants.user import USER_TYPES
from idm.users.fetchers import IDSGroupFetcher
from idm.users.managers import (
    BaseIDMUsersManager,
    GroupManager,
    GroupResponsibilityManager,
    GroupMembershipManager,
    OrganizationManager,
)
from idm.users.queues import GroupQueue
from idm.users.utils import get_user_fullname, get_localized_group_name, get_group_url
from idm.utils import http
from idm.core.utils import create_or_update_model
from idm.utils.i18n import get_lang_key
from idm.utils.model_groups import transform_to_groups, group_by_scope
from idm.utils.model_helpers import get_abstract_model_without_some_fields
from idm.utils.symmetric_difference import get_symmetric_difference

log = logging.getLogger(__name__)


class User(get_abstract_model_without_some_fields(AbstractUser, ['first_name', 'last_name', 'date_joined'])):
    fired_at = models.DateField(null=True, blank=True)                      # уволен
    contract_ends_at = models.DateField(null=True, blank=True)              # дата окончания контракта из OEBS
    nda_ends_at = models.DateField(null=True, blank=True)                   # дата окончания NDA из OEBS
    idm_found_out_dismissal = models.DateTimeField(null=True, blank=True)   # время, когда IDM узал об увольнении

    # эти поля перенесены из модели Profile
    SEX_CHOICES = (
        ('M', 'male'),
        ('F', 'female'),
    )
    uid = models.TextField(null=True, db_index=True)
    guid = models.CharField(max_length=47, null=True, default=None, unique=True)
    sex = models.CharField(max_length=1, choices=SEX_CHOICES, null=True)
    staff_id = models.IntegerField(null=True)
    imported_at = models.DateTimeField(null=True, db_index=True)
    date_joined = models.DateField(null=True, db_index=True)

    type = models.TextField(_('Тип пользователя'), choices=USER_TYPES.CHOICES, db_index=True, default=USER_TYPES.USER)

    center_id = models.IntegerField(default=0, db_index=True)
    lang_ui = models.CharField(max_length=10, default='ru')
    first_name = models.TextField(default='', blank=True)
    last_name = models.TextField(default='', blank=True)
    first_name_en = models.TextField(default='', blank=True)
    last_name_en = models.TextField(default='', blank=True)
    ldap_active = models.NullBooleanField(default=None)  # если False - уволен в LDAP
    ldap_blocked = models.BooleanField(default=False)  # если True - блокирован в LDAP, но не уволен
    ldap_blocked_timestamp = models.DateTimeField(null=True, blank=True) # время первой успешной блокировки в LDAP
    is_absent = models.BooleanField(default=False)  # True - будет отсутствовать более 10 часов в ближайшие сутки
    is_homeworker = models.BooleanField(default=False)
    is_robot = models.BooleanField(default=False)
    mobile_phone = models.CharField(blank=True, null=True, max_length=15)
    updated = models.DateTimeField(auto_now_add=True)
    department = StrictForeignKey('Department',
                                   null=True,
                                   related_name='users2',
                                   on_delete=models.SET_NULL)
    department_group = StrictForeignKey('Group', null=True, blank=True, related_name='employees',
                                         on_delete=models.SET_NULL)
    position = models.CharField(max_length=150, default='')

    affiliation = models.CharField(max_length=8, db_index=True, choices=AFFILIATION.CHOICES,
                                   null=False, blank=False, default=AFFILIATION.OTHER)
    responsibles = models.ManyToManyField('self', symmetrical=False, related_name='robots')

    notify_responsibles = models.BooleanField(default=False)  # уведомлять ответственных за робота о событиях с ролями
    is_frozen = models.BooleanField(default=False, db_index=True)

    objects = BaseIDMUsersManager()

    USERNAME_FIELD = 'username'
    REQUIRED_FIELDS = ['email']

    class Meta:
        verbose_name = _('user')
        verbose_name_plural = _('users')
        db_table = 'upravlyator_user'
        unique_together = [('username', 'type')]

    def get_absolute_url(self):
        return '/user/%s/' % self.username

    @property
    def external_id(self):
        return self.username

    @property
    def timezone(self):
        return 'Europe/Moscow'

    @property
    def for_doctest(self):
        return 'user("%s")' % self.username

    @property
    def member_of(self):
        return Group.objects.filter(memberships__user=self, memberships__state__in=GROUPMEMBERSHIP_STATE.ACTIVE_STATES)

    @property
    def active_responsibilities(self):
        return GroupResponsibility.objects.filter(user=self, is_active=True)

    @property
    def responsible_in(self):
        return Group.objects.filter(responsibilities__user=self, responsibilities__is_active=True)

    @property
    def is_tvm_app(self):
        return self.type == USER_TYPES.TVM_APP

    def has_perm(self, perm, obj=None):
        from idm.core.models import InternalRole

        result = super(User, self).has_perm(perm, obj)

        if not result:
            try:
                permission_obj = get_permission(perm)
            except Permission.DoesNotExist:
                result = False
            else:
                result = InternalRole.objects.filter(
                    user_object_permissions__user=self,
                    node=None,
                    user_object_permissions__permission=permission_obj,
                ).exists()

        return result

    def is_female(self):
        """Возвращает True, если пол пользователя женский"""
        return self.sex == 'F'

    def is_idm_robot(self):
        return self.username == settings.IDM_ROBOT_USERNAME

    def is_external(self):
        """
        Метод возвращает True, если пользователь не внутренний.
        Название сохранено для обратной совместимости.
        """
        return self.affiliation not in AFFILIATION.INTERNAL_VALUES

    def get_short_name(self, lang: str = None):
        lang = lang or get_lang_key()
        if lang == 'ru':
            return self.first_name
        else:
            return self.first_name_en

    def get_full_name(self, lang: str = None) -> str:
        return get_user_fullname(self, lang)

    @property
    def departments_chain(self):
        """
        Список департаментов, в которых состоит сотрудник. Возвращает в порядке от листьев к корню дерева
        """
        if self.department_group is None:
            return Group.objects.none()

        groups = (
            self.department_group
            .get_ancestors(include_self=True)
            .filter(level__gt=0)
            .prefetch_related('responsibilities', 'responsibilities__user')
            .order_by('-level')
        )
        return groups

    @property
    def head(self):
        """Возвращает непосредственного начальника сотрудника"""
        try:
            return self.all_heads[0]
        except IndexError:
            pass

    @property
    def all_heads(self):
        """Возвращает кортеж всех начальников сотрудника"""

        heads = [
            group.head for group in self.departments_chain
            if group.head != self and group.head is not None
        ]
        return heads

    @property
    def internal_head(self):
        ancestor_responsibilities = self.department_group.get_ancestor_responsibilities(include_self=True)
        internal_head_responsibilities = ancestor_responsibilities.filter(
            rank=ranks_constants.HEAD, user__affiliation__in=AFFILIATION.INTERNAL_VALUES, is_active=True
        ).exclude(user=self)
        if internal_head_responsibilities:
            return internal_head_responsibilities.order_by('-group__level').first().user

    @cached_property
    def actual_mobile_phone(self):
        """
        Номер телефона не из базы (возможно устаревший), а напрямую из центра.
        В случае недоступности фолбечимся на локальный номер.
        """
        if self.is_tvm_app:
            raise AccessDenied(_('У TVM приложения не может быть номера телефона'))

        repo = registry.get_repository(
            'staff', 'person',
            oauth_token=settings.IDM_STAFF_OAUTH_TOKEN,
            user_agent=settings.IDM_IDS_USER_AGENT,
            timeout=settings.IDM_IDS_TIMEOUT,
        )
        try:
            data = repo.get_one({'login': self.username, '_fields': 'phones'})
        except (http.RequestException, ValueError):
            log.exception('Failed to fetch actual phone number for %s', self.username)
            return self.mobile_phone
        phones = data.get('phones')
        if not phones:
            return None
        main_phone = None
        numbers = []
        for phone_data in phones:
            if phone_data['is_main']:
                main_phone = phone_data['number']
            numbers.append(phone_data['number'])
        if main_phone is None:
            main_phone = numbers[0]

        mobile_phone = re.sub('[^+0-9]', '', main_phone)
        if mobile_phone and self.mobile_phone != mobile_phone:
            # Сохраняем локально
            self.mobile_phone = mobile_phone
            self.save(update_fields=('mobile_phone',))
        return self.mobile_phone

    def get_membership_query(self):
        """Возвращает список групп, включая родительские, где данный пользователь участник.
        """
        qs = Group.objects.active().filter(
            parent__isnull=False,
            groupclosure_children__child__memberships__user=self,
            groupclosure_children__child__memberships__state__in=GROUPMEMBERSHIP_STATE.ACTIVE_STATES,
        )
        return qs

    def get_managed_tvm_apps(self):
        """Возвращает спиок TVM_APP'ов, находящихся в TMV_SERVICE'ах, где данный пользователь ответственный.
        """
        if any(self.has_internal_role(role) for role in ('superuser', 'developer')):
            tvm_apps = User.objects.active().filter(type=USER_TYPES.TVM_APP)
        else:
            tvm_service_groups = Group.objects.filter(
                responsibilities__user=self,
                responsibilities__is_active=True,
                type=GROUP_TYPES.TVM_SERVICE
            )
            tvm_apps = User.objects.active().filter(
                memberships__group__in=tvm_service_groups,
                memberships__state=GROUPMEMBERSHIP_STATE.ACTIVE,
                type=USER_TYPES.TVM_APP,
            ).distinct()
        return tvm_apps

    def get_responsibility_query(self, include_ancestors=False):
        """Возвращает список групп, включая дочерние, где данный пользователь ответственный.
        Если include_ancestors=True, то в выборку включаются также группы,
        """
        ancestors_q = Q(
            groupclosure_children__child__responsibilities__user=self,
            groupclosure_children__child__responsibilities__is_active=True
        )
        descendants_q = Q(
            groupclosure_parents__parent__responsibilities__user=self,
            groupclosure_parents__parent__responsibilities__is_active=True,
        )
        qs = Group.objects.active().filter(parent__isnull=False)
        if include_ancestors:
            qs = qs.filter(descendants_q | ancestors_q)
        else:
            qs = qs.filter(descendants_q)
        return qs

    def get_subordinates(self, ranks=ranks_constants.ALL_HEAD_TYPES):
        """Возвращает список пользователей, у которых данный пользователь – ответственный в их department_group,
        с рангом rank, или родительских для department_group"""

        qs = User.objects.users().filter(
            department_group__groupclosure_parents__parent__responsibilities__user=self,
            department_group__groupclosure_parents__parent__responsibilities__is_active=True,
            department_group__groupclosure_parents__parent__responsibilities__rank__in=ranks,
        ).distinct()
        return qs

    def is_head_for(self, user, ranks=(ranks_constants.HEAD,)):
        """Является ли пользователь начальником (возможно не непосредственным)
        для пользователя user."""

        if self.department_group_id is None or user.type == USER_TYPES.TVM_APP:
            return False
        qs = (
            user.department_group.get_ancestors(include_self=True).
            filter(responsibilities__user=self,
                   responsibilities__rank__in=ranks,
                   responsibilities__is_active=True)
        )
        return qs.exists()

    def is_owner_of(self, user):
        if user.type == USER_TYPES.USER and user.is_robot:
            return self.robots.filter(pk=user.pk).exists()

        elif user.type == USER_TYPES.TVM_APP:
            return GroupResponsibility.objects.filter(
                group_id__in=GroupMembership.objects.active().filter(user=user).values('group_id'),
                user=self,
            ).active().exists()

        return False

    def drop_permissions_cache(self):
        """Очищает кеш пермишенов
        """
        for attr in ('_perm_cache', '_user_perm_cache'):
            if hasattr(self, attr):
                delattr(self, attr)

    def get_all_roles(self, node, system_specific=False):
        """Возвращает все роли, которые выданы на узел node, возможно учитывая system_specific"""
        from idm.core.models import Role

        role_filter = {
            'system': node.system,
            'node': node,
            'state_set': 'active',
            'users': [self],
        }
        if system_specific is not False:
            role_filter['system_specific'] = system_specific

        filter_q, _ = Role.objects.get_filtered_query(**role_filter)
        roles = Role.objects.filter(filter_q)
        return roles

    def has_role(self, node, system_specific=False):
        """
        Вернуть bool, есть ли у пользователя роль с данными {role_data}.
        В случае, если не передан параметр {system_specific}, совпадение по полю system_specific не учитывается.
        :type role_data: dict
        :type system_specific: NoneType | dict
        :rtype: bool
        """
        roles = self.get_all_roles(node, system_specific=system_specific)
        return roles.exists()

    def has_internal_role(self, role, system=None, scope='/'):
        """Проверяет наличие роли у пользователя
        """
        from idm.core.models import InternalRoleUserObjectPermission

        qs = InternalRoleUserObjectPermission.objects.filter(user=self, content_object__role=role)

        q = Q(content_object__node=None)

        if system:
            q |= Q(content_object__node__system=system, content_object__node__value_path=scope)

        return qs.filter(q).exists()

    def get_permissions(self, system=None, scope=None):
        """
        Возвращает все внутренние пермишены.
        Делает это через union, чтобы не делать сложный запрос с OR в условии.
        """
        base_qs = (
            self.internalroleuserobjectpermission_set
            .values_list('permission__codename', flat=True)
            .distinct()
        )
        qs = base_qs.filter(content_object__node=None)

        if system:
            if scope:
                q = Q(
                    content_object__node=scope,
                    content_object__node__level=scope.level,
                    content_object__node__system=system
                )
            else:
                q = Q(
                    content_object__node__level=0,
                    content_object__node__system=system
                )
            qs = qs.union(base_qs.filter(q))

        permissions = set(qs)
        if system and system.creator == self:
            permissions.update(set(settings.IDM_SYSTEM_ROLES_PERMISSIONS['responsible']))

        return list(permissions)

    def get_systems_with_responsibility(self):
        """Возвращает все системы, где пользователь ответственный"""

        from idm.core.models import System

        qs = System.objects.filter(
            root_role_node__internal_roles__role__in=['roles_manage', 'responsible'],
            root_role_node__internal_roles__user_object_permissions__user=self,
        ).distinct()
        return qs

    def get_permissions_by_systems(self):
        from idm.core.models import System

        system_perms_with_systems = (self.internalroleuserobjectpermission_set.filter(content_object__node__level=0).
            values_list('content_object__node__system__slug', 'permission__codename').
            order_by('content_object__node__system').
            distinct()
        )
        global_perms = (
            self.internalroleuserobjectpermission_set.filter(content_object__node__isnull=True).
            values_list('permission__codename', flat=True).distinct()
        )

        perms_with_systems = defaultdict(set)
        global_perms = list(global_perms)
        if global_perms:
            perms_with_systems = {
                slug: set(global_perms) for slug in System.objects.order_by('slug').values_list('slug', flat=True)
            }
        for system_slug, permission in system_perms_with_systems:
            perms_with_systems[system_slug].add(permission)

        perms = {slug: sorted(perms) for slug, perms in perms_with_systems.items()}
        return perms

    def add_responsibles(self, responsibles):
        '''Добавляет ответственных за робота'''
        if not isinstance(responsibles, Iterable):
            responsibles = [responsibles]
        self.responsibles.add(*responsibles)
        from idm.core.models import Action
        actions = [Action(action='robot_responsible_added',
                          user_id=responsible.pk,
                          robot_id=self.pk,
                          ) for responsible in responsibles]
        Action.objects.bulk_create(actions)

    def remove_responsibles(self, responsibles):
        '''Удаляет ответственных за робота'''
        if not isinstance(responsibles, Iterable):
            responsibles = [responsibles]
        self.responsibles.remove(*responsibles)
        from idm.core.models import Action
        actions = [Action(action='robot_responsible_removed',
                          user_id=responsible.pk,
                          robot_id=self.pk,
                          ) for responsible in responsibles]
        Action.objects.bulk_create(actions)

    def get_passport_login(self):
        passport_logins_count = self.passport_logins.count()
        if passport_logins_count == 1:
            login = self.passport_logins.get()
            return login.login
        elif passport_logins_count == 0:
            return self.passport_logins.get_available_login(self)
        raise MultiplePassportLoginsError(
            'User %s already have %s passport_logins' % (self.username, passport_logins_count)
        )

    def send_email_about_attach_passport_login_to_membership(self, templates, context):
        from idm.notification.utils import send_reminder
        from idm.core.workflow.common.subject import subjectify

        subject = subjectify(self)
        log.info(
            'Sending reminder about membership that need attach passport_login to the %(subject)s'
            % {'subject': subject.get_ident()}
        )
        try:
            send_reminder(subject, templates, **context)
            return True
        except Exception:
            log.exception(
                'Error while trying to send reminder about membership'
                'that need attach passport_login to the %(subject)s',
                subject.get_ident()
            )


class Group(LocalizedModel, ExternalTrackingNode, DifferenceTableMixin):
    GROUP_CHOICES = (
        (GROUP_TYPES.DEPARTMENT, _('Департамент')),
        (GROUP_TYPES.SERVICE, _('Сервисная группа')),
        (GROUP_TYPES.TVM_SERVICE, _('TVM сервис')),
        (GROUP_TYPES.WIKI, _('Вики-группа')),
    )

    created_at = models.DateTimeField(_('Дата создания'), default=timezone.now, editable=False, null=True)
    updated_at = models.DateTimeField(_('Дата обновления'), editable=False, null=True)
    expire_at = models.DateTimeField(_('Дата отзыва'), editable=False, null=True)

    external_id = models.IntegerField(verbose_name=_('Внешний ID'), default=0)

    name = models.CharField(_('Имя группы'), max_length=255, default='', db_index=True)
    name_en = models.CharField(_('Имя группы (на английском)'), max_length=255, default='', db_index=True)
    description = models.TextField(blank=True, default='', verbose_name=_('Описание'))
    type = models.CharField(_('Тип группы'), max_length=255, choices=GROUP_CHOICES, db_index=True, default='department')

    objects = GroupManager()
    fetcher = IDSGroupFetcher()
    queue_class = GroupQueue
    EXTERNAL_ID_FIELD = 'external_id'
    REQUIRE_EXTERNAL_ID = True
    SUPPORTS_RESTORE = True

    class Meta:
        verbose_name = _('Группа')
        verbose_name_plural = _('Группы')

    def __str__(self):
        return self.name

    def get_name(self, lang=None):
        return get_localized_group_name(self, lang)

    def as_canonical(self):
        if self.type == GROUP_TYPES.DEPARTMENT:
            # Состав департаментных групп обновляем в таске по синку пользователей
            memberships = []
        else:
            # Используем итерацию с условием, а не фильтр, т.к. часть нужных полей запрефетчили выше
            memberships = [
                membership.as_canonical()
                for membership
                in self.memberships.all()
                if membership.state == 'active' and membership.is_direct
            ]
        responsibilities = [
            responsibility.as_canonical()
            for responsibility
            in self.responsibilities.all()
            if responsibility.is_active
        ]
        members = {membership.as_key(): membership for membership in memberships}
        responsibilities = {responsibility.as_key(): responsibility for responsibility in responsibilities}
        return canonical.CanonicalGroup(
            hash=self.hash,
            slug=self.slug,
            type=self.type,
            name=self.name,
            name_en=self.name_en,
            parent_id=self.parent.external_id if self.parent else None,
            external_id=self.external_id,
            members=members,
            description=self.description,
            responsibles=responsibilities,
            children=(),
        )

    @classmethod
    def from_canonical(cls, canonical):
        group = cls(
            hash=canonical.hash,
            slug=canonical.slug,
            type=canonical.type,
            name=canonical.name,
            name_en=canonical.name_en,
            description=canonical.description,
            external_id=canonical.external_id,
        )
        return group

    def as_snapshot(self):
        qs = (
            self.get_ancestors(include_self=True).
            filter(level__gt=0).
            values('id', 'external_id', 'slug', 'name', 'name_en')
        )
        return list(qs)

    @property
    def head(self):
        # todo: deprecate
        heads = [
            resp.user for resp in self.responsibilities.all()
            if resp.is_active and resp.rank == ranks_constants.HEAD
        ]
        head_ = None
        if heads:
            head_ = heads[0]
        return head_

    @property
    def for_doctest(self):
        return 'group(%s)' % self.external_id

    @property
    def members(self) -> QuerySet[User]:
        return User.objects.filter(
            memberships__group=self,
            memberships__state__in=GROUPMEMBERSHIP_STATE.ACTIVE_STATES,
            memberships__is_direct=True,
            is_active=True,
        )

    @property
    def responsibles(self):
        return User.objects.filter(responsibilities__group=self, responsibilities__is_active=True)

    @property
    def active_responsibilities(self):
        return GroupResponsibility.objects.filter(group=self, is_active=True)

    def save(self, *args, **kwargs):
        now = timezone.now()
        if self.pk is None:
            self.created_at = now
        self.updated_at = now
        return super(Group, self).save(*args, **kwargs)

    def build_path(self, for_parent=''):
        if for_parent != '':
            parent = for_parent
        else:
            parent = self.parent
        if parent is None:
            path = '%s%d%s' % (self.SEP, self.external_id, self.SEP)
        else:
            path = '%s%d%s' % (parent.path, self.external_id, self.SEP)
        return path

    def synchronize(self):
        result, queue = super(Group, self).synchronize()
        refreshed = type(self).objects.get(pk=self.pk)
        refreshed.save(update_fields=('updated_at',))
        # необходимо, чтобы обновить updated_at
        return result, queue

    def mark_depriving(self, **kwargs):
        valid_until = timezone.now() + timezone.timedelta(hours=settings.IDM_DEPRIVING_GROUP_HOURS)
        self.expire_at = valid_until
        self.state = 'depriving'
        descendants = self.get_descendants(include_self=True).filter(state='active')
        descendants.update(expire_at=valid_until, state='depriving', hash='')
        return self

    def add_members(self, members: List['CanonicalMember']):
        from idm.core.models import Action
        now = timezone.now()
        staff_ids = [member.staff_id for member in members]
        # Метод работает только для пользователей, но не tvm-приложений
        user_ids = list(User.objects.users().filter(center_id__in=staff_ids).values_list('pk', flat=True))
        if len(user_ids) < len(members):
            raise SynchronizationError('Unknown member in group %d' % self.external_id)

        memberships = []
        for user_id in user_ids:
            memberships.append(create_or_update_model(
                model=GroupMembership,
                obj_filter={
                    'user_id': user_id,
                    'group_id': self.pk,
                    'is_direct':True
                },
                defaults={
                    'state': GROUPMEMBERSHIP_STATE.ACTIVE,
                    'date_joined': now,
                    'date_leaved': None,
                }
            ))

        membership_actions = [Action(action='user_joined_group',
                                     user_id=membership.user_id,
                                     group_id=membership.group_id,
                                     membership=membership,
                                     ) for membership in memberships]
        Action.objects.bulk_create(membership_actions)
        memberships = (
            GroupMembership.objects
            .filter(pk__in=(x.pk for x in memberships))
            .select_related('user', 'user__department_group')
        )
        signals.memberships_added.send(sender=self, memberships=memberships, group=self)

    def add_responsibles(self, responsibles):
        from idm.core.models import Action
        now = timezone.now()
        external_ids = {responsible.staff_id for responsible in responsibles}
        external_to_internal = dict(User.objects.filter(center_id__in=external_ids).values_list('center_id', 'pk'))
        if len(external_ids) > len(external_to_internal):
            raise SynchronizationError('Unknown responsible in group %d' % self.external_id)
        responsibilities = []
        for responsible in responsibles:
            user_id = external_to_internal.get(responsible.staff_id)
            responsibilities.append(create_or_update_model(
                model=GroupResponsibility,
                obj_filter={
                    'user_id': user_id,
                    'rank': responsible.rank,
                    'group_id': self.pk,
                },
                defaults={
                    'is_active': True,
                    'date_joined': now,
                    'date_leaved': None,
                }
            ))
        responsibility_actions = [Action(action='group_responsible_added',
                                         user_id=responsibility.user_id,
                                         data={'rank': responsibility.rank},
                                         group_id=responsibility.group_id,
                                         responsibility=responsibility) for responsibility in responsibilities]
        Action.objects.bulk_create(responsibility_actions)
        signals.responsibilities_added.send(sender=self, responsibilities=responsibilities, group=self)

    def remove_members(self, members: Union[List['CanonicalMember'], str],
                       group_is_deleted=False, action_data: dict = None):
        """
        :param members: Список участников групп на удаление. Значение '*' значит удалить всех
        """
        from idm.core.models import Action
        now = timezone.now()
        # Используем итерацию с условием, а не фильтр, т.к. часть нужных полей запрефетчили выше
        memberships = [
            membership
            for membership
            in self.memberships.all()
            if membership.state == GROUPMEMBERSHIP_STATE.ACTIVE and membership.is_direct
        ]
        if members != '*':
            membership_keys = {member.as_key() for member in members}
            memberships = [membership for membership in memberships
                           if membership.as_canonical().as_key() in membership_keys]
        if action_data is None:
            action_data = {}
        if group_is_deleted:
            action_data['group_is_deleted'] = True

        actions = []
        for membership in memberships:
            action = Action(
                action='user_quit_group',
                user_id=membership.user_id,
                group_id=membership.group_id,
                membership=membership,
                data=action_data
            )
            actions.append(action)
        Action.objects.bulk_create(actions)
        signals.memberships_removed.send(
            sender=self,
            memberships=memberships,
            group=self,
            group_is_deleted=group_is_deleted,
        )
        for membership in memberships:
            membership.date_leaved = now
            membership.state = GROUPMEMBERSHIP_STATE.INACTIVE
            membership.save(update_fields=('date_leaved', 'state'))

    def remove_responsibles(self, responsibles, group_is_deleted=False, action_data=None):
        from idm.core.models import Action
        now = timezone.now()
        responsibilities = [
            responsibility
            for responsibility
            in self.responsibilities.all()
            if responsibility.is_active
        ]
        if responsibles != '*':
            responsibilities_keys = {responsibility.as_key() for responsibility in responsibles}
            responsibilities = [responsibility for responsibility in responsibilities
                                if responsibility.as_canonical().as_key() in responsibilities_keys]
        if action_data is None:
            action_data = {}
        if group_is_deleted:
            action_data['group_is_deleted'] = True

        actions = []
        for responsibility in responsibilities:
            action_data['rank'] = responsibility.rank
            action = Action(
                action='group_responsible_removed',
                user_id=responsibility.user_id,
                group_id=responsibility.group_id,
                data=action_data,
                responsibility=responsibility,
            )
            actions.append(action)
        Action.objects.bulk_create(actions)
        signals.responsibilities_removed.send(sender=self, responsibilities=responsibilities, group=self)
        for responsibility in responsibilities:
            responsibility.date_leaved = now
            responsibility.is_active = False
            responsibility.save(update_fields=('date_leaved', 'is_active'))

    # method has no non-db side-effects
    @atomic_retry
    def deprive(self):
        if self.state != 'depriving':
            return False

        self.remove_members(
            '*',
            group_is_deleted=True,
            action_data={'comment': 'User has quit the group because group has been deleted'},
        )
        self.remove_responsibles(
            '*',
            group_is_deleted=True,
            action_data={'comment': 'User stopped to be responsible for the group because group has been deleted'},
        )

        signals.group_deprived.send(sender=self, group=self)
        self.actions.create(action='group_deleted')

        self.state = 'deprived'
        self.expire_at = None
        self.save(update_fields=('state', 'expire_at'))
        return True

    def get_roles(self, node, system_specific=False, include_ancestors=False):
        from idm.core.models import Role

        if include_ancestors:
            groups = self.get_ancestors(include_self=True)
        else:
            groups = [self]

        role_filter = {
            'system': node.system,
            'node': node,
            'state_set': 'active',
            'groups': groups,
        }
        if system_specific is not False:
            role_filter['system_specific'] = system_specific

        filter_q, _ = Role.objects.get_filtered_query(**role_filter)
        return Role.objects.filter(filter_q)

    def has_role(self, node, system_specific=False):
        """
        Вернуть bool, есть ли у группы роль с данными {role_data}.
        В случае, если не передан параметр {system_specific}, совпадение по полю system_specific не учитывается.

        :type role_data: dict
        :type system_specific: NoneType | dict
        :rtype: bool
        """
        roles = self.get_roles(node, system_specific, include_ancestors=False)
        return roles.exists()

    def get_external_url(self):
        return get_group_url(self)

    def is_managed_by(self, user, ranks=(ranks_constants.HEAD,)):
        qs = GroupResponsibility.objects.filter(user=user,
                                                rank__in=ranks,
                                                group__in=self.get_ancestors(include_self=True),
                                                is_active=True)
        return qs.exists()

    def is_service_scope(self):
        return self.type == GROUP_TYPES.SERVICE and self.level == 2

    def get_responsibles(self, ranks=None):
        if self.is_service_scope():
            return self.parent.get_responsibles(ranks)

        if ranks is None:
            ranks = (ranks_constants.HEAD,)
        qs = User.objects.filter(responsibilities__group=self,
                                 responsibilities__rank__in=ranks,
                                 responsibilities__is_active=True
        ).order_by('username').distinct()
        return qs

    def get_ancestor_responsibilities(self, up_to_level=None, ranks=None, include_self=True):
        ancestors = self.get_ancestors(include_self=include_self)
        qs = (
            GroupResponsibility.objects.
                active().
                select_related('group', 'user').
                filter(group__in=ancestors)
        )
        if up_to_level is not None:
            if up_to_level <= 0:
                up_to_level = self.level + up_to_level
                if up_to_level < 0:
                    up_to_level = 0
            qs = qs.filter(group__level__gte=up_to_level)
        if ranks:
            qs = qs.filter(rank__in=ranks)
        return qs

    def get_descendant_responsibles(self, include_self=True, ranks=None):
        qs = User.objects.filter(responsibilities__group__in=self.get_descendants(include_self=include_self),
                                 responsibilities__is_active=True)
        qs = qs.order_by('username')
        if ranks:
            qs = qs.filter(responsiblities__group__rank__in=ranks)
        return qs

    def get_descendant_members(self):
        qs = User.objects.filter(memberships__group=self, memberships__state__in=GROUPMEMBERSHIP_STATE.ACTIVE_STATES)
        qs = qs.order_by('username')
        return qs

    def get_unique_memberships_count(self):
        memberships = self.get_descendant_memberships()
        count = memberships.values_list('user_id').distinct().count()
        return count

    def get_descendant_memberships(self, include_self=True):
        qs = GroupMembership.objects.filter(
            group__in=self.get_descendants(include_self=include_self),
            state__in=GROUPMEMBERSHIP_STATE.ACTIVE_STATES,
            user__is_active=True
        )
        qs = qs.select_related('user', 'group')
        qs = qs.order_by('user__username')
        return qs

    def get_full_name(self, separator=' -> '):
        ancestors = self.get_ancestors(include_self=True)
        path = separator.join([ancestor.name for ancestor in ancestors if not ancestor.is_root_node()])
        return path

    def has_afterfilling_roles(self):
        # Отдает True, если у группы активные есть роли, выданные на ноды,
        # имеющие поле с типом "passportlogin"
        from idm.core.models import RoleNode
        nodes_pks = set(self.roles.active().values_list('node__rolenodeclosure_parents__parent_id', flat=True))
        return RoleNode.objects.filter(
            pk__in=nodes_pks,
            fields__type=FIELD_TYPE.PASSPORT_LOGIN
        ).exists()

    def send_email_about_attach_passport_login_to_membership(self, role):
        context = {'group_external_id': self.external_id, 'group_name': self.name, 'role': role}
        success_notified = []

        memberships = (
            self
            .memberships
            .filter(state__in=GROUPMEMBERSHIP_STATE.ACTIVE_STATES, passport_login__isnull=True)
            .select_related('user')
        )

        for membership in memberships:
            result = membership.user.send_email_about_attach_passport_login_to_membership(
                PASSPORT_LOGIN_NEED_ATTACH_NEW_GROUP_ROLE_TEMPLATES,
                context,
            )
            if result:
                success_notified.append(membership.pk)
        self.memberships.update_notified_about_passport_login(success_notified)

    def get_changed_roles_for_new_root(self, new_root=None, membership_inheritance=None):
        from idm.api.frontend.role import AbstractRoleResource
        from idm.core.models import Role

        changes = self.get_changes_for_new_root(new_root=new_root, membership_inheritance=membership_inheritance)
        groups_bag = set(changes.keys())
        for table in changes.values():
            groups_bag.update(table['obtained'])
            groups_bag.update(table['lost'])

        group_to_roles = defaultdict(set)
        roles = (
            Role.objects
            .active().
            select_related(*AbstractRoleResource.Meta.select_related_for_list)
            .get_roles_of_groups(groups_bag)
        )
        for role in roles:
            group_to_roles[role.group_id].add(role)

        for table in changes.values():
            table['ancestors'].obj = group_to_roles[table['ancestors'].obj.id]

        for key_group, table in changes.items():
            table['obtained'] = set(chain(*[group_to_roles[group.id] for group in table['obtained']]))
            table['lost'] = set(chain(*[group_to_roles[group.id] for group in table['lost']]))
            table['obtained'], table['lost'] = get_symmetric_difference(
                table['obtained'],
                table['lost'],
                wrapper_class=RoleWrapperForSet,
            )
            inherited_roles = set(chain(*table['ancestors']))
            table['lost'], _ = get_symmetric_difference(
                table['lost'],
                inherited_roles,
                wrapper_class=RoleWrapperForSet,
            )
            table['obtained'], _ = get_symmetric_difference(
                table['obtained'],
                inherited_roles,
                wrapper_class=RoleWrapperForSet,
            )
            del table['ancestors']
        return changes

    def get_changes_of_service_group_for_new_root(self, new_root=None, membership_inheritance=None):
        """ Метод только для сервисных групп """
        changes = self.get_changes_without_scopes_for_new_root(
            new_root=new_root, membership_inheritance=membership_inheritance
        )
        if not changes:
            return changes
        obtained_scopes = group_by_scope(Group.objects.get_scopes_of_groups(changes[self]['obtained']))
        lost_scopes = group_by_scope(Group.objects.get_scopes_of_groups(changes[self]['lost']))
        scopes = Group.objects.get_scopes_of_groups(list(changes.keys()))
        for group in scopes:
            changes[group] = {
                'obtained': obtained_scopes[group.get_scope()],
                'lost': lost_scopes[group.get_scope()],
                'ancestors': changes[group.parent]['ancestors'] + group
            }

        return changes

    def get_changes_without_scopes_for_new_root(self, new_root=None, membership_inheritance=None):
        service = self.get_service()
        service_changes = service.get_changes_for_new_root(
            new_root=new_root.get_service() if new_root else None,
            membership_inheritance=membership_inheritance,
        )
        if not service_changes:
            return service_changes
        services_bag = (set(service_changes.keys())
                        | set(service_changes[service]['lost'])
                        | set(service_changes[service]['obtained']))
        groups_slug = ['svc_' + service.slug for service in services_bag]
        group_bag = Group.objects.filter(slug__in=groups_slug)
        service_to_group = {group.get_service_slug(): group for group in group_bag}
        result = {service_to_group[service.slug]: table for service, table in service_changes.items()}
        transform_to_groups(result[self]['obtained'], service_to_group)
        transform_to_groups(result[self]['lost'], service_to_group)
        for table in result.values():
            table['ancestors'].obj = service_to_group[table['ancestors'].obj.slug]
        result = {group: table.copy() for group, table in result.items()}
        return result

    def get_service(self):
        service_slug = self.get_service_slug()
        return GroupService.objects.get(slug=service_slug)

    def get_service_slug(self):
        assert(self.type == GROUP_TYPES.SERVICE)
        service_slug = self.slug.replace('svc_', '', 1)
        return service_slug

    def get_scope(self):
        parent_slug = self.parent.slug
        return self.slug.replace(parent_slug + '_', '', 1)

    def get_tree_parent(self):
        if self.type == GROUP_TYPES.SERVICE:
            parent_slug = 'svc_' + self.get_service().parent.slug
            return Group.objects.get(slug=parent_slug)
        elif self.type == GROUP_TYPES.DEPARTMENT:
            return self.parent

    def get_unordered_tree_descendants(self, include_self=False):
        if self.type == GROUP_TYPES.SERVICE:
            descendants_services = self.get_service().get_descendants(include_self=include_self)
            descendants_slugs = ['svc_' + slug for slug in descendants_services.values_list('slug', flat=True)]
            return Group.objects.filter(slug__in=descendants_slugs)
        elif self.type == GROUP_TYPES.DEPARTMENT:
            return self.get_descendants(include_self=include_self)


class RoleWrapperForSet(object):
    def __init__(self, role):
         self.role = role

    @property
    def obj(self):
        return self.role

    def __eq__(self, o):
        return (
            self.role.node_id == o.role.node_id and
            self.role.fields_data == o.role.fields_data and
            self.role.system_specific == o.role.system_specific and
            self.role.parent_id == o.role.parent_id
        )

    def __hash__(self):
        return hash(self.role.node_id)


class Subject(models.Model):
    SUBJECT_TYPES = (
        ('user', _('Пользователь')),
        ('group', _('Группа')),
    )
    id = models.IntegerField(primary_key=True)
    user = StrictForeignKey(User, verbose_name=_('Пользователь'), null=True, on_delete=models.DO_NOTHING)
    group = StrictForeignKey(Group, verbose_name=_('Группа'), null=True, on_delete=models.DO_NOTHING)
    username = models.CharField(_('Логин или URL'), max_length=255, default='')
    first_name = models.CharField(_('Имя'), max_length=255, default='')
    last_name = models.CharField(_('Фамилия'), max_length=255, default='')
    parent_name = models.CharField(_('Имя родительской группы'), max_length=255, default='')
    full_name = models.CharField(_('Название'), max_length=255, default='')
    full_name_en = models.CharField(_('Название на английском'), max_length=255, default='')
    is_group = models.BooleanField(_('Группа'), default=False)
    subject_type = models.CharField(_('Тип объекта'), max_length=255, choices=SUBJECT_TYPES, null=True)
    group_type = models.CharField(_('Тип группы'), max_length=255, choices=Group.GROUP_CHOICES, null=True)
    external_id = models.CharField(_('ID в Стаффе'), max_length=255, default='')

    class Meta:
        managed = False
        db_table = 'users_subject'
        verbose_name = _('Объект выдачи роли')
        verbose_name_plural = _('Объекты выдачи роли')

    def __str__(self):
        if self.is_group:
            result = _('Группа с url "%(url)s" и внешним id "%(external_id)s"') % {
                'url': self.username,
                'external_id': self.external_id
            }
        else:
            result = _('Пользователь "%(username)s" с внешним id "%(external_id)s"') % {
                'username': self.username,
                'external_id': self.external_id
            }
        return result

    def get_full_name(self):
        if get_lang_key() == 'ru':
            result = self.full_name.strip()
        else:
            result = self.full_name_en.strip()
        if not result:
            result = self.username.strip()
        return result


class GroupMembership(models.Model):
    user = StrictForeignKey(
        User, verbose_name=_('Пользователь'), related_name='memberships', null=False, on_delete=models.CASCADE,
    )
    group = StrictForeignKey(
        Group, verbose_name=_('Группа'), related_name='memberships', null=False, on_delete=models.CASCADE,
    )
    date_joined = models.DateTimeField(verbose_name=_('Дата вступления в группу'), null=True, default=None)
    date_leaved = models.DateTimeField(verbose_name=_('Дата выхода из группы'), null=True, default=None)
    state = models.TextField(
        verbose_name='Состояние',
        choices=GROUPMEMBERSHIP_STATE.NAMES,
        default=GROUPMEMBERSHIP_STATE.INACTIVE,
        db_index=True,
    )
    is_direct = models.BooleanField(db_index=True)
    notified_about_passport_login = models.NullBooleanField(db_index=True, default=False, null=True)
    passport_login = StrictForeignKey(
        'core.UserPassportLogin',
        verbose_name=_('Паспортный логин'),
        related_name='group_memberships',
        null=True, default=None,
        on_delete=models.CASCADE,
    )

    objects = GroupMembershipManager()

    class Meta:
        verbose_name = _('Членство пользователя в группе')
        verbose_name_plural = _('Членства пользователей в группах')
        unique_together = (('user', 'group', 'is_direct'),)
        index_together = (('user', 'group', 'state'),)

    def __str__(self):
        result = _('Членство пользователя {user:s} в группе {group:s}').format(user=self.user.username,
                                                                               group=self.group.name)
        return result

    def as_canonical(self):
        return canonical.CanonicalMember(staff_id=self.user.center_id, state=self.state)

    def deprive_roles_with_old_logins(self):
        from idm.core.models import Role, Action
        old_roles = (
            Role.objects
            .filter(
                user_id=self.user_id,
                parent__group_id=self.group_id,
                parent__is_active=True,
            )
            .select_related('node', 'parent', 'parent__system', 'parent__node', 'parent__group', 'user')
        )

        if self.passport_login:
            old_roles = old_roles.exclude(passport_logins__pk=self.passport_login.pk)

        # Создаём аналогичные роли, но с новым логином (или в awaiting без него)
        new_roles = []
        parent_roles = {role.parent for role in old_roles}
        for role in parent_roles:
            new_role = role.create_group_member_role(self.user, replace_login=True)
            if new_role:
                new_roles.append(new_role.pk)

        robot = User.objects.get_idm_robot()
        roles_to_deprive = list(old_roles.deprivable().exclude(pk__in=new_roles))
        if roles_to_deprive:
            parent_action = Action.objects.create(
                action=ACTION.MASS_ACTION,
                data={'name': 'deprive_roles_with_old_logins'}
            )

            for role in roles_to_deprive:
                # Отзываем только те роли, к которым был привязан логин
                if role.fields_data.get('passport-login'):
                    role.deprive_or_decline(
                        robot,
                        comment='Отзываем роль, выданную по членству с неактуальным паспортным логином',
                        to_replace=True,
                        parent_action=parent_action,
                    )

    def poke_awaiting_personal_roles(self):
        from idm.core.models import Role

        login = self.passport_login
        if not login:
            return

        awaiting_roles = (
            Role.objects
            .awaiting()
            .filter(
                user_id=self.user_id,
                parent__group_id=self.group_id,
            )
            .select_related('system', 'user', 'node')
        )
        for role in awaiting_roles:
            if role.passport_logins.exists():
                continue  # цепляем логин только если раньше никаких логинов не было
            try:
                role.system.check_passport_policy(role.user, role.node, direct_passport_login=login)
            except PassportLoginPolicyError:
                continue
                # добавление логина нарушает установленные политики, в таком случае логин не прицепляется
                # роль пока не трогаем, но при ближайшем ежедневном синке она уйдёт в ошибку
            try:
                role.add_passport_login(login.login, approve=True)
            except Exception:
                log.exception(
                    'Could not attach login "%s" to awaiting personal role with pk="%s"',
                    login.login,
                    role.pk,
                )

    def user_is_able_to_assign_passport_login(self, user):
        return (
            user == self.user or
            user.has_perm('core.assign_passport_login') or
            user.is_head_for(self.user)
        )


class GroupResponsibility(models.Model):
    RANKS = (
        (ranks_constants.HEAD, _('Глава')),
        (ranks_constants.DEPUTY, _('Заместитель')),
        (ranks_constants.HR_PARTNER, _('HR-партнёр')),
        (ranks_constants.BUDGET_HOLDER, _('Держатель бюджета')),
        (ranks_constants.GENERAL_DIRECTOR, _('Генеральный директор')),
    )

    user = StrictForeignKey(
        User, verbose_name=_('Пользователь'), related_name='responsibilities', null=False, on_delete=models.CASCADE
    )
    group = StrictForeignKey(Group, verbose_name=_('Группа, в которой пользователь является ответственным'),
                              related_name='responsibilities', null=False, on_delete=models.CASCADE)
    rank = models.CharField(choices=RANKS, max_length=50, db_index=True, verbose_name=_('Статус в группе'), null=True)
    date_joined = models.DateTimeField(_('Дата начала ответственности за группу'), null=True, default=None)
    date_leaved = models.DateTimeField(_('Дата окончания ответственности за группу'), null=True, default=None)
    is_active = models.BooleanField(default=False, db_index=True)

    objects = GroupResponsibilityManager()

    class Meta:
        verbose_name = _('Ответственность пользователя в группе')
        verbose_name_plural = _('Ответственности пользователей в группах')
        unique_together = (('user', 'group', 'rank'),)
        index_together = (('user', 'group', 'rank', 'is_active'),)

    def __str__(self):
        return 'Статус пользователя %(user)s в группе %(group)s [%(status)s] (%(active)s)' % {
            'user': self.user.username,
            'group': self.group.name,
            'status': self.get_rank_display(),
            'active': 'on' if self.is_active else 'off',
        }

    def as_canonical(self):
        return canonical.CanonicalGroupResponsibility(
            staff_id=self.user.center_id,
            rank=self.rank
        )


class Department(models.Model):
    """ Эти модели наполняем информацией со стаффа:
        http://staff.yandex.ru/staff/deps.xml
    """
    name = models.CharField(max_length=255)
    slug = models.CharField(max_length=255)
    parent = StrictForeignKey('self', null=True, related_name='subdeps', on_delete=models.CASCADE)
    chief = StrictForeignKey(User, null=True, related_name='head_of', on_delete=models.CASCADE)
    users = models.ManyToManyField(User, related_name='departments')
    added = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    deleted = models.BooleanField(default=False)

    class Meta:
        db_table = 'upravlyator_department'

    def __repr__(self):
        return '<Department: %d, %s>' % (self.id, self.name.encode('utf-8'))

    def __str__(self):
        names = [self.name]
        item = self

        while item.parent:
            item = item.parent
            names.insert(0, item.name)

        return ' ⇾ '.join(names)

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = md5((str(random.random()) + settings.SECRET_KEY).encode('utf-8')).hexdigest()
        return super(Department, self).save(*args, **kwargs)


class Organization(models.Model):
    org_id = models.IntegerField(null=False, db_index=True)
    is_active = models.BooleanField(default=True)
    last_success_dismissed_users_sync_at = models.DateTimeField(null=True, blank=True)
    removed_at = models.DateTimeField(null=True, blank=True)

    objects = OrganizationManager()

    def deactivate(self):
        from idm.core.models import Action

        if not self.is_active:
            return

        with transaction.atomic():
            self.removed_at = timezone.now()
            self.is_active = False
            self.save(update_fields=['removed_at', 'is_active'])
            Action.objects.create(organization=self, action=ACTION.ORGANIZATION_REMOVED)
            self.remove_users()

    def add_user(self, user):
        membership = self.memberships.filter(user=user).first()
        if membership and not membership.is_active:
            membership.is_active = True
            membership.removed_at = None
            membership.save(update_fields=['is_active', 'removed_at'])
            membership.actions.create(action=ACTION.USER_JOINED_ORGANIZATION)
        elif not membership:
            membership = self.memberships.create(user=user)
            membership.actions.create(action=ACTION.USER_JOINED_ORGANIZATION)

        return membership

    def remove_user(self, user):
        membership = self.memberships.filter(user=user).first()
        if membership and membership.is_active:
            membership.actions.create(action=ACTION.USER_QUIT_ORGANIZATION)
            membership.is_active = False
            membership.removed_at = timezone.now()
            membership.save(update_fields=['is_active', 'removed_at'])

    def remove_users(self, uids=None):
        from idm.core.models import Action

        filter_query = Q(organization=self, is_active=True)
        if uids is not None:
            filter_query &= Q(user__username__in=uids)

        with transaction.atomic():
            active_memberships = OrganizationMembership.objects.filter(filter_query)
            actions = [
                Action(organization_membership=membership, action=ACTION.USER_QUIT_ORGANIZATION)
                for membership in active_memberships
            ]
            active_memberships.update(is_active=False, removed_at=timezone.now())
            Action.objects.bulk_create(actions)

    def __str__(self):
        return 'Organization %d' % self.org_id


class OrganizationMembership(models.Model):
    user = StrictForeignKey(User, related_name='organization_memberships', on_delete=models.CASCADE)
    organization = StrictForeignKey(Organization, related_name='memberships', on_delete=models.CASCADE)
    is_active = models.BooleanField(default=True)
    removed_at = models.DateTimeField(null=True, blank=True)


class B2BAdmin(models.Model):
    user = StrictForeignKey(User, related_name='b2b_admins', on_delete=models.CASCADE)
    external_login = models.CharField(
        null=False,
        unique=True,
        max_length=50,
        db_index=True,
        blank=False,
    )
    internal_login = models.CharField(
        null=False,
        max_length=50,
        db_index=True,
        blank=False,
    )

    def __str__(self):
        return '%s' % self.internal_login


@receiver(signals.memberships_added)
def grant_user_role_on_join(memberships, group, **kwargs):
    # TODO: перенести в синхронизацию пользователей
    if group.type == 'department':
        for membership in memberships:
            user = membership.user
            source_group = user.department_group
            user.department_group = group
            user.save(update_fields=('department_group',))

            if source_group != group:
                log.info('%s moved from %s to %s',
                         user.username,
                         'none' if source_group is None else source_group.slug,
                         group.slug)

                user.actions.create(
                    action='user_change_department',
                    data={
                        'department_from': None if source_group is None else source_group.id,
                        'dep_name_from': 'Не указан' if source_group is None else source_group.name,
                        'department_to': group.id,
                        'dep_name_to': group.name,
                    }
                )

                if source_group is not None:
                    user.transfers.create(
                        type='user',
                        state='undecided',
                        source=source_group,
                        target=group,
                        source_name=source_group.as_snapshot(),
                        target_name=group.as_snapshot(),
                    )


@receiver(signals.group_deprived)
def deprive_group_roles(group, **kwargs):
    from idm.core.models import Action

    parent_action = Action.objects.create(
        action=ACTION.MASS_ACTION,
        data={
            'name': 'deprive_group_roles',
            'group_id': group.id,
        }
    )
    group.roles.hold_or_decline_or_deprive(parent_action=parent_action)


@receiver(signals.group_restored)
def restore_group_roles(group, **kwargs):
    from idm.core.models import Action

    parent_action = Action.objects.create(
        action=ACTION.MASS_ACTION,
        data={
            'name': 'deprive_group_roles',
            'group_id': group.id,
        }
    )
    group.roles.select_related('system', 'node').restore_holded_on_group_restore(parent_action=parent_action)
