# coding: utf-8
from typing import List

import celery
import constance.settings
import waffle
from constance import config

from django.conf import settings
from django.db import transaction, OperationalError
from django.db.models import Q, F
from django.template.loader import render_to_string
from django.utils.translation import override
from django_pgaas import atomic_retry
from ylog.context import log_context
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import force_text

from idm.celery_app import app
from idm.core import depriving
from idm.core.constants.action import MAX_ACTION_ERROR_LENGTH, ACTION
from idm.core.constants.batchrequest import BATCH_REQUEST_TYPE
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.role import ROLE_STATE
from idm.core.constants.passport_login import CREATED_FLAG_REASON, PASSPORT_LOGIN_STATE
from idm.core.constants.system import SYSTEM_ROLE_GRANT_POLICY
from idm.core.workflow.exceptions import (
    PassportLoginPolicyError,
    RoleRequestError,
    BrokenSystemError,
    Forbidden,
)
from idm.core.exceptions import (
    PushDisabled,
    PassportLoginGenerationError,
    MultiplePassportLoginsError,
    MultiplePassportLoginUsersError,
)
from idm.core.models import (
    Role,
    System,
    Action,
    UserPassportLogin,
    DelayedRoleRequest,
)
from idm.core.plugins.errors import BasePluginError, PluginFatalError
from idm.framework.requester import requesterify, Requester
from idm.framework.task import BaseTask, UnrecoverableError, DelayingError
from idm.inconsistencies.models import Inconsistency
from idm.sync import passport
from idm.users.models import User
from idm.utils import chunkify
from idm.utils.cleansing import cleanup_fields_data
from idm.utils.lock import lock
from idm.utils.tasks import get_object_or_fail_task, get_object_or_retry_task
from idm.utils.log import log_duration
from idm.notification.sms import send_sms


class Task(BaseTask):
    """Базовый таск для плагинов.

    В нем реализованы методы для подтверждения
    и отзыва роли, а так же для автоматической пометки
    ее как failed в случае если выброшено исключение
    UnrecoverableError.
    """

    retry_queue = 'roles_retry'
    monitor_success = False  # таски этого типа запускаются ad-hoc и слишком часто

    # method has no non-db side effects
    @atomic_retry
    def finish(self, role_id=None, system_specific=None, passport_login_created=False, secret_context=None, state=ROLE_STATE.GRANTED,
               **kwargs):
        """ Последний шаг, либо подтверждение, либо отметка о посылке роли в систему """
        role = get_object_or_fail_task(Role.objects.basic_related(), pk=role_id)
        role.lock(for_task=True)
        role.system_specific = system_specific
        role.save(update_fields=('system_specific',))
        role.set_state(state, comment=kwargs.get('comment'))
        if state == ROLE_STATE.AWAITING:  # TODO: https://st.yandex-team.ru/IDM-8073
            return {
                'step': 'send_email_about_attach_passport_login_to_membership',
                'role_id': role_id,
            }
        else:
            return {
                'step': 'send_email_with_results',
                'state': state,
                'role_id': role_id,
                'action_id': kwargs['action_id'],
                'passport_login_created': passport_login_created,
                'secret_context': secret_context,
            }

    # method has no non-db side effects
    @atomic_retry
    def deprive(self, role_id=None, **kwargs):
        """ Метод, деактивирующий роль. """

        role = get_object_or_fail_task(Role.objects.basic_related(), pk=role_id)
        if role.state != ROLE_STATE.DEPRIVED:
            role.set_state(ROLE_STATE.DEPRIVED, from_any_state=True, comment=kwargs.get('comment'))

        return {
            'step': 'unsubscribe_passport_login',
            'state': 'deprived',
            'role_id': role_id,
            'action_id': kwargs['action_id']
        }

    # method has non-db side effects, they are idempotent
    @atomic_retry
    def unsubscribe_passport_login(self, state=None, role_id=None, action_id=None, **kwargs):
        from idm.core.tasks.passport import UnsubscribePassportLogin
        role = get_object_or_fail_task(Role, pk=role_id)
        if role.passport_logins.exists():
            passport_login = role.passport_logins.get()
            if not passport_login.roles.filter(is_active=True).exists():
                UnsubscribePassportLogin.delay(passport_login=passport_login.login)

        nextstep = {
            'step': 'send_email_with_results',
            'state': state,
            'role_id': role_id,
            'action_id': action_id,
        }
        nextstep.update(kwargs)
        return nextstep

    # method has no non-db side effects
    @atomic_retry
    def _failed(self, exc_val, kwargs, retry=False):
        # Занесем сообщение об ошибке в базу
        action_id = kwargs.get('action_id')
        if action_id is not None:
            action = (
                Action.objects
                .basic_role_related()
                .select_related('role__last_request__requester', 'role__parent')
                .filter(pk=action_id)
                .first()
            )
            if action is not None:
                message = force_text(exc_val)
                if isinstance(exc_val, BasePluginError):
                    state = ROLE_STATE.FAILED
                    action_name = ACTION.FAIL
                    action_comment = _('Не удалось добавить роль в систему из-за ошибки в системе.')
                elif isinstance(exc_val, PushDisabled):
                    return
                else:
                    state = ROLE_STATE.IDM_ERROR
                    action_name = ACTION.IDM_ERROR
                    action_comment = _('Не удалось добавить роль в систему из-за ошибки в IDM.')

                if retry and action.role.system.retry_failed_roles and not isinstance(exc_val, PluginFatalError):
                    action.role.create_action(
                        action_name,
                        action_comment,
                        error=message,
                        requester=requesterify(User.objects.get_idm_robot()),
                    )
                else:
                    action.role.set_state(state, error=message,
                                          passport_login_created=kwargs.get('passport_login_created', False))

    # method has non-db side effects, could not retry it!
    @transaction.atomic
    def send_email_with_results(self, state=None, role_id=None, action_id=None, passport_login_created=False,
                                secret_context=None, **kwargs):
        action = get_object_or_retry_task(Action, pk=action_id)
        if action.data.get('dont_send_notofication_with_results'):
            return
        role_qs = Role.objects.basic_related().select_related('last_request__requester', 'parent__last_request__requester', 'parent__system', 'depriver', 'parent__node')
        role = get_object_or_retry_task(role_qs, pk=role_id)
        role.send_email_with_results(
            action,
            passport_login_created=passport_login_created,
            context=secret_context
        )

        if state == 'granted':
            role_context = {
                'passport_login': role.fields_data.get('passport-login') if role.fields_data else None,
                'passport_login_created': passport_login_created,
            }
            if isinstance(role.fields_data, dict):
                role_context.update(role.fields_data)

            if isinstance(role.system_specific, dict):
                role_context.update(role.system_specific)

            if role.is_user_role():
                # шлём sms только людям :)
                return {
                    'step': 'send_sms_with_results',
                    'role_id': role_id,
                    'role_context': role_context,
                    'secret_context': secret_context,
                }

    # method has non-db side effects, could not retry it!
    @transaction.atomic
    def send_sms_with_results(self, role_id: int, role_context: dict, secret_context: dict, **_):
        """Отправляет СМС о выдаче роли, если необходимо"""
        role_qs = Role.objects.basic_related()
        role = get_object_or_fail_task(role_qs, pk=role_id)
        if not (role.state == 'granted' and role.send_sms):
            return
        elif role.system.slug == config.YANDEX_ID_SYSTEM_SLUG and not secret_context:  # IDM-12033
            return

        user = role.user
        system = role.system

        sms_context = {
            'user': user,
            'role': role,
        }

        if isinstance(role_context, dict):
            sms_context.update(role_context)

        if isinstance(secret_context, dict):
            sms_context.update(secret_context)

        with override(user.lang_ui):
            message = render_to_string(
                [
                    'sms/role_granted_%s.txt' % system.slug,
                    'sms/role_granted.txt'
                ],
                sms_context
            )
            message = message.strip()
            # RULES-903 По возможности используем актуальный номер со стаффа
            phone = user.actual_mobile_phone

            if phone and message:
                self.log.info('sending SMS for role %s, login %s, phone %s: %s',
                               role_id, user.username, phone, settings.SEND_SMS_URL)
                success, info = send_sms(message, phone, user.username)

                if success:  # сообщение успешно отправлено
                    self.log.info('SMS sending response for role %s, login %s, phone %s: %s',
                                   role_id, user.username, phone, info)

                    return

                self.log.error('SMS did not send to %s for role_id=%s at phone %s: %s',
                                user.username, role_id, phone, info)

    # method has non-db side effects, could not retry it!
    @transaction.atomic
    def send_email_about_attach_passport_login_to_membership(self, role_id=None, **kwargs):
        role = get_object_or_fail_task(Role.objects.basic_related().select_related('parent', 'parent__group'), pk=role_id)
        if role.parent is None or role.parent.group is None:
            return

        group = role.parent.group
        user = role.user

        if not self.check_awaiting_role_before_send_message(role, user, group):
            return
        query = Q(notified_about_passport_login=True)
        query |= Q(passport_login__isnull=False)
        query &= Q(group=group, state=GROUPMEMBERSHIP_STATE.ACTIVE)
        if user.memberships.filter(query).exists():
            # Мы уже отправили письмо про эту группу раньше или логин уже привязан
            return
        context = {'group_external_id': group.external_id, 'group_name': group.name, 'role': role}
        result = user.send_email_about_attach_passport_login_to_membership(
                PASSPORT_LOGIN_NEED_ATTACH_NEW_GROUP_ROLE_TEMPLATES,
                context,
            )
        if result:
            (
                user
                .memberships
                .filter(
                    group=group,
                    state=GROUPMEMBERSHIP_STATE.ACTIVE
                )
                .update(notified_about_passport_login=True)
            )

    def check_awaiting_role_before_send_message(self, role, user, group):
        is_valid = True
        if role.state != 'awaiting':
            self.log.warning(
                'Attempt to send mail about attach passport_login for role (id=%s) not in `awaiting` state',
                role.id,
            )
            is_valid = False
        if user is None:
            self.log.warning(
                'Attempt to send mail about attach passport_login for not user role (id=%)',
                role.id,
            )
            is_valid = False
        if group is None:
            self.log.warning(
                'Attempt to send mail for role (id=%s) about attach passport_login, bat parent role not group role',
                role.id,
            )
            is_valid = False
        return is_valid


class RegisterPassportLoginMixin(object):
    def init_attributes(self, membership=None, role=None):
        if not any([membership, role]) or all([membership, role]):
            raise Exception('Any of membership or role should be passed, and not both')
        self.subscribe = False
        self.passport_login_created = False
        if membership:
            self.user = membership.user
            self.role = None
        if role:
            role.fetch_system()
            role.fetch_user()
            self.subscribe = role.is_subscribable(ignore_state=True)
            self.role = role
            self.user = role.user

    def register_passport_login(self, passport_login):
        self.log.info('Trying to register passport login %s', passport_login)
        passport.register_login(
            login=passport_login,
            username=self.user.username,
            first_name=self.user.first_name,
            last_name=self.user.last_name,
            subscribe=self.subscribe,
        )
        self.log.info('Passport login %s successfully registered', passport_login)
        return True

    def check_or_register_login(self, passport_login):
        if passport.exists(passport_login):
            try:
                login = UserPassportLogin.objects.get(login=passport_login)
                if self.role:
                    if login.state != PASSPORT_LOGIN_STATE.SUBSCRIBED and login.is_subscribable(new_role=self.role):
                        # Может быть снята подписка из-за отзыва всех предыдущих ролей
                        login.subscribe()

            except UserPassportLogin.DoesNotExist:
                # RULES-2232
                self.log.exception('Passport login <%s> registered but not found in UserPassportLogin',
                                   passport_login)
                raise UnrecoverableError(
                    'Попытка использовать логин, неизвестный IDM. Выберите другой логин.'
                )

        else:
            self.passport_login_created = self.register_passport_login(passport_login)


class RoleAdded(RegisterPassportLoginMixin, Task):
    # method has no non-db side effects
    @atomic_retry
    def init(self, system_id=None, action_id=None, distinct_exclude=None, role_id=None, **kwargs):
        if role_id is not None:
            role = get_object_or_fail_task(Role.objects.select_related('parent', 'node'), pk=role_id)
            system_id = role.system_id
            action_id = role.actions.filter(action=ACTION.APPROVE).order_by('-added').values_list('pk', flat=True).first()
            if action_id is None:
                raise UnrecoverableError(f'No approve action for approved role {role.id}', action_id=action_id)
            # Сюда приходим из допинывалки, которая точно допинывает approved роли
            # Если роль не в approved, то с ней уже что-то произошло
            if role.state != ROLE_STATE.APPROVED:
                self.log.warning('Role %s is already processed', role.id)
                return

        else:
            action = get_object_or_retry_task(Action.objects.select_related('role', 'role__parent', 'role__node'), pk=action_id)
            role = action.role

        if role.parent and role.parent.state not in ROLE_STATE.ALMOST_ACTIVE_STATES:
            raise UnrecoverableError(
                'Невозможно запросить роль, если родительская роль в неактивном состоянии',
                action_id=action_id
            )
        passport_login = role.get_passport_login()

        if passport_login:
            nextstep = {
                'step': 'add_passport_login',
                'passport_login': passport_login,
                'system_id': system_id,
                'action_id': action_id,
                'distinct_exclude': distinct_exclude,
            }
        elif not passport_login and role.should_have_login(is_required=True):
            assert role.is_personal_role(), 'User role passed fields validation but lacks the passport login'
            nextstep = {
                'step': 'get_or_generate_passport_login',
                'role_id': role.pk,
                'action_id': action_id,
                'system_id': system_id,
                'distinct_exclude': distinct_exclude,
            }
        else:
            nextstep = {
                'step': 'add_role',
                'system_id': system_id,
                'action_id': action_id,
                'distinct_exclude': distinct_exclude,
            }
        return nextstep

    def get_or_generate_passport_login(self, action_id, role_id, system_id, distinct_exclude, **kwargs):
        role = get_object_or_fail_task(Role.objects.basic_related(), pk=role_id)
        user = role.user
        try:
            login = user.get_passport_login()
            self.log.info(
                'Role %s requires passport_login, but it is not added to group, '
                'login %s will be used (create if it is missing)',
                role.id, login,
            )
            nextstep = {
                'step': 'add_passport_login',
                'passport_login': login,
                'system_id': system_id,
                'action_id': action_id,
                'distinct_exclude': distinct_exclude,
            }
            return nextstep
        except PassportLoginGenerationError:
            self.log.info(
                'User %s does not have passport_login, all automatically generated already exist, '
                'create a login failed, role %s state changed to awaiting',
                user.username, role.id,
            )
            comment = 'У пользователя нет паспортного логина, создать новый не получилось'
        except MultiplePassportLoginsError:
            self.log.info(
                'User %s has several passport_logins, '
                'but none of them is tied to group, role %s state changed to awaiting',
                user.username, role.id,
            )
            comment = 'К членству в группе не привязан паспортный логин'
        nextstep = {
            'step': 'add_role',
            'system_id': system_id,
            'action_id': action_id,
            'distinct_exclude': distinct_exclude,
            'passport_login_created': False,
            'comment': comment,
        }
        return nextstep

    def add_passport_login(self, passport_login, system_id, action_id, distinct_exclude, **kwargs):
        """Добавляет паспортный логин во внешний паспорт, если его там нет"""

        action_qs = Action.objects.select_related(
            'role',
            'role__user',
            'role__node',
            'role__system',
            'role__parent',
        )
        action = get_object_or_retry_task(action_qs, pk=action_id)
        role = action.role
        self.init_attributes(role=role)
        user = role.user

        try:
            role.system.check_passport_policy(user, role.node, direct_passport_login=passport_login)
        except PassportLoginPolicyError as e:
            raise UnrecoverableError(str(e))

        with transaction.atomic():
            try:
                self.check_or_register_login(passport_login)
                login = role.add_passport_login(passport_login, approve=False)
                role.add_passport_login_to_membership(login)
                if self.passport_login_created:
                    login.created_by_idm = True
                    login.created_flag_reason = CREATED_FLAG_REASON.EXACT
                    login.save(update_fields=['created_by_idm', 'created_flag_reason'])
                    if role.is_subscribable(ignore_state=True):
                        login.set_state(PASSPORT_LOGIN_STATE.SUBSCRIBED)
                        login.actions.create(action='subscribed', data={'comment': 'Логин подписан при создании'})
            except Exception as err:
                self.log.exception('Cannot save passport login <%s> state for role <%s>', passport_login, role.pk)
                raise DelayingError(str(err))

        return {
            'step': 'add_role',
            'system_id': system_id,
            'action_id': action_id,
            'passport_login_id': login.pk,
            'passport_login_created': self.passport_login_created,
            'distinct_exclude': distinct_exclude,
        }

    def add_role(self, system_id=None, action_id=None, passport_login_id=None, passport_login_created=False, distinct_exclude=None,  comment=None, **kwargs):
        system = get_object_or_fail_task(System, pk=system_id)
        action_qs = Action.objects.select_related(
            'role',
            'role__node',
            'role__user',
            'role__group',
        )

        action = get_object_or_retry_task(action_qs, pk=action_id)
        role = action.role

        if role.state != ROLE_STATE.APPROVED:
            self.log.warning('Role %s is already processed', role.id)
            return
        elif role.user_id is not None and not role.user.is_active:
            raise UnrecoverableError(
                'Attempted to add role of an inactive user.'
            )

        target_state, comments = role.check_awaiting_conditions(passport_login_id=passport_login_id, comment=comment)

        if target_state == ROLE_STATE.AWAITING:
            return {
                'step': 'close_st_issue',
                'request_id': role.last_request_id,
                'state': ROLE_STATE.AWAITING,
                'role_id': role.pk,
                'action_id': action_id,
                'passport_login_created': passport_login_created,
                'comment': '\n'.join(map(str, comments)),
            }

        result = {
            'step': 'close_st_issue',
            'request_id': role.last_request_id,
            'state':
                ROLE_STATE.GRANTED
                if system.role_grant_policy == SYSTEM_ROLE_GRANT_POLICY.IDM else
                ROLE_STATE.SENT,
            'role_id': role.pk,
            'action_id': action_id,
            'passport_login_created': passport_login_created,
        }

        if distinct_exclude is None:
            distinct_exclude = ('system_specific', 'parent')
        if not role.is_unique(ignore=distinct_exclude, among_states=ROLE_STATE.ACTIVE_RETURNABLE_STATES):
            alike = role.get_alike(ignore=distinct_exclude, among_states=ROLE_STATE.ACTIVE_RETURNABLE_STATES)
            # Копируем system_specific из идентичных ролей. Все активные роли с совпадающими
            # user/group, system, data и fields_data должны иметь и одинаковые system_specific данные.
            # Иногда (при фиксе неконсистентностей, например) бывает необходимо пушить
            # даже при уникальности только по родителям
            result['system_specific'] = alike.first().system_specific
            result['comment'] = 'Роль выдана без оповещения системы'
        else:
            params = {
                'id': role.pk,
                'path': role.node.slug_path,
                'role_data': role.node.data,
                'fields_data': role.fields_data,
                'with_inheritance': role.with_inheritance,
                'with_robots': role.with_robots,
                'with_external': role.with_external,
                'request_id': action_id,
                'unique_id': role.node.unique_id,
            }
            if role.is_user_role():
                params['username'] = str(role.user.username)
                params['uid'] = role.user.uid
                if system.use_tvm_role:
                    params['subject_type'] = role.user.type
            else:
                params['group_id'] = role.group.external_id

            first_push_action = role.actions.filter(action=ACTION.FIRST_ADD_ROLE_PUSH, added__gt=action.added).first()
            if first_push_action is None:
                role.actions.create(action=ACTION.FIRST_ADD_ROLE_PUSH)

            try:
                with transaction.atomic():
                    role.lock(for_task=True, nowait=True)
                    data = system.plugin.add_role(**params)
            except OperationalError:
                # Не смогли взять лок, попробуем еще через минуту, так как допинывалка все равно придет
                self.log.warning('Role %s is already locked', role.pk)
                return

            system_specific = data.get('data')
            context = data.get('context')
            result['secret_context'] = context
            if system_specific is not None:
                # заменяем точки на дефисы в полученных паспортных логинах и не храним пароли
                system_specific = cleanup_fields_data(system_specific)
            result['system_specific'] = system_specific
        return result

    def close_st_issue(self, request_id=None, **kwargs):
        from idm.core.models import RoleRequest
        if request_id is not None:
            request = RoleRequest.objects.get(pk=request_id)
            request.close_issue()

        return {
            'step': 'finish',
            **kwargs,
        }


class RequestRoleRefs(Task):
    def init(self, action_id):
        action_qs = Action.objects.select_related(
            'role',
            'role__system',
            'role__user',
            'role__group',
            'role__node',
        )
        action = get_object_or_retry_task(action_qs, pk=action_id)
        role = action.role
        self.log.info('Trying to request refs for role %d', role.pk)
        role.request_refs()
        self.log.info('Requested refs for role %d', role.pk)


class DepriveRoleRefs(Task):
    def init(self, action_id, bypass_checks=False, comment=None, user=None):
        action = get_object_or_retry_task(Action.objects.select_related('role'), pk=action_id)
        role = action.role
        self.log.info('Trying to deprive refs for role %d', role.pk)
        role.deprive_refs(bypass_checks=bypass_checks, comment=comment, user=user)
        self.log.info('Deprived refs for role %d', role.pk)


class DontMakeRoleFailed(Task):
    # no non-db side-effects
    @atomic_retry
    def _failed(self, exc_val, kwargs, retry=False):
        action_id = kwargs.get('action_id', None)
        if action_id:
            action = get_object_or_retry_task(Action, pk=action_id)
            action.error = force_text(exc_val)[:MAX_ACTION_ERROR_LENGTH]
            if isinstance(exc_val, BasePluginError):
                action.data['code'] = exc_val.code
            elif isinstance(exc_val, PushDisabled):
                return
            else:
                action.data['code'] = settings.DEFAULT_SYSTEM_ERROR_CODE
            action.save(update_fields=['error', 'data'])


class RoleRemoved(DontMakeRoleFailed):
    def init(self, slug, action_id, with_push=True, **kwargs):
        action_qs = Action.objects.select_related(
            'role',
            'role__node',
            'role__user',
            'role__group',
            'role__system',
        )

        action = get_object_or_retry_task(action_qs, pk=action_id)
        role = action.role

        if waffle.switch_is_active('stop_depriving'):
            self.log.warning('Depriving is turned off globally')
            return

        if role.system.stop_depriving:
            self.log.warning('Depriving is turned off for system %s', role.system.slug)
            return

        comment = None

        # удаляем роль в системе только в том случае, если она была активна в системе
        # и она является уникальной для пользователя. У пользователя может быть несколько
        # одинаковых ролей, но отзываться должна лишь последняя
        if (
            with_push and
            (
                role.state in ROLE_STATE.DEPRIVABLE_STATES and
                role.is_unique(ignore=['parent'], among_states=ROLE_STATE.ACTIVE_RETURNABLE_STATES)
            )
        ):
            params = {
                'id': role.pk,
                'path': role.node.slug_path,
                'role_data': role.node.data,
                'fields_data': role.fields_data,
                'system_specific': role.system_specific,
                'request_id': action_id,
                'unique_id': role.node.unique_id,
            }
            if role.is_user_role():
                params.update({
                    'username': str(role.user.username),
                    'uid': role.user.uid,
                    'is_fired': not role.user.is_active
                })
                if role.system.use_tvm_role:
                    params['subject_type'] = role.user.type
            else:
                params.update({
                    'group_id': role.group.external_id,
                    'is_fired': not role.group.is_active(),
                })
            plugin = role.system.plugin

            # Для следующих попыток будет REDEPRIVE, в таких случаях не пишем экшен про первую попытку
            if action.action in (ACTION.DEPRIVE, ACTION.EXPIRE):
                first_push_action = role.actions.filter(action=ACTION.FIRST_REMOVE_ROLE_PUSH, added__gt=action.added).first()
                if first_push_action is None:
                    role.actions.create(action=ACTION.FIRST_REMOVE_ROLE_PUSH)

            with transaction.atomic():
                role.lock(for_task=True)
                plugin.remove_role(**params)
        else:
            comment = 'Роль удалена без оповещения системы'

        return {
            'step': 'deprive',
            'role_id': role.id,
            'action_id': action_id,
            'comment': comment
        }


class DepriveInconsistentRole(DontMakeRoleFailed):
    """Для отзыва ролей, не заведенных в базе"""

    @transaction.atomic
    def init(self, inconsistency_id, action_data=None, requester=None, **kwargs):
        inconsistency_qs = Inconsistency.objects.select_related(
            'node',
            'user',
            'group',
            'system',
        )
        inconsistency = get_object_or_fail_task(inconsistency_qs, pk=inconsistency_id)

        with log_context(inconsistency_id=inconsistency_id, system=inconsistency.system.slug):
            inconsistency.resolve_nonexistent_role(requester=requester, action_data=action_data)

        return {
            'step': 'resolve_inconsistency',
            'inconsistency_id': inconsistency_id,
            'action_data': action_data,
        }

    @transaction.atomic
    def resolve_inconsistency(self, inconsistency_id, action_data=None, **kwargs):
        inconsistency_qs = Inconsistency.objects.select_related(
            'user',
            'group',
        )
        inconsistency = get_object_or_fail_task(inconsistency_qs, pk=inconsistency_id)
        comment = None
        if action_data is not None:
            comment = action_data.get('comment')
        inconsistency.set_resolved(action_data=action_data, comment=comment)


class DepriveInconsistentRoleAndRequestNew(DepriveInconsistentRole):
    def init(self, inconsistency_id, action_data=None, requester=None, **kwargs):
        super().init(inconsistency_id, action_data=None, requester=None, **kwargs)
        return {
            'step': 'request_new',
            'inconsistency_id': inconsistency_id,
            'action_data': action_data,
        }

    @transaction.atomic
    def request_new(self, inconsistency_id, action_data=None, **kwargs):
        inconsistency_qs = Inconsistency.objects.select_related(
            'node',
            'user',
            'group',
            'system__actual_workflow',
        )
        inconsistency = get_object_or_fail_task(inconsistency_qs, pk=inconsistency_id)
        robot = User.objects.get_idm_robot()
        subject = inconsistency.get_subject()
        self.log.info('Requesting new role instead of inconsistency id %d', inconsistency_id)
        role = Role.objects.request_role(
            requester=robot,
            subject=subject,
            system=inconsistency.system,
            comment=action_data.get('comment'),
            data=inconsistency.node,
            fields_data=inconsistency.remote_fields,
            inconsistency=None if inconsistency.is_forced else inconsistency,
        )
        self.log.info('Requested new role %d instead of inconsistency %d', role.pk, inconsistency_id)
        role.expire_alike()
        self.log.info('We have made roles that are like %d expired', role.pk)

        return {
            'step': 'resolve_inconsistency',
            'inconsistency_id': inconsistency_id,
            'action_data': action_data,
        }


class RerunWorkflow(BaseTask):
    monitor_success = False  # таски этого типа запускаются ad-hoc и слишком часто

    @atomic_retry
    def init(self, role_id, parent_action_id=None):
        with log_context(role_id=role_id), log_duration(self.log, 'Rerunning workflow for role %d', role_id):
            role = get_object_or_fail_task(
                Role.objects.basic_related().select_related('system__actual_workflow', 'parent'),
                pk=role_id
            )
            robot = User.objects.get_idm_robot()
            parent_action = Action.objects.get(id=parent_action_id)
            role.rerun_workflow(robot, parent_action=parent_action)


class RequestNewSystemResponsibles(BaseTask):
    monitor_success = False  # таски этого типа запускаются ad-hoc и слишком часто

    def init(self, system_id):
        system = get_object_or_fail_task(System.objects.select_related('creator'), pk=system_id)
        creator = system.creator
        self_system = System.objects.get_idm_system()
        try:
            self_system.synchronize(user=creator, force_update=True)
        except:
            self.log.info('Self system sync failed')
            raise DelayingError


class PokeAwaitingRoles(BaseTask):
    monitor_success = False  # таски этого типа запускаются ad-hoc

    def init(self, system_id=None, organization_id=None, fields=None):
        query = Q()
        if system_id is not None:
            query &= Q(system_id=system_id)
        if organization_id is not None:
            query &= Q(organization_id=organization_id)
        if fields is not None:
            assert isinstance(fields, dict), '`fields` must be a dict'
            query &= Q(**{'fields_data__%s' % field: value for field, value in fields.items()})
        parent_action = Action.objects.create(
            action=ACTION.MASS_ACTION,
            data={'name': 'poke_awaiting_roles'}
        )
        Role.objects.poke_awaiting_roles(query=query, parent_action=parent_action)


class DepriveRolesWhenDetachingResource(BaseTask):
    monitor_success = False  # таски этого типа запускаются ad-hoc

    def init(self, role_id):
        role = get_object_or_fail_task(Role, pk=role_id)
        Role.objects.remove_roles_when_detaching_resource(role)


class CheckDeprivingRoles(BaseTask):
    CHUNK_SIZE = 100

    def init(self):
        role_data_by_parent_group = depriving.group_roles(depriving.get_depriving_roles())['by_parent_group']
        total_depriving_roles = sum(len(roles_data) for roles_data in role_data_by_parent_group.values())
        depriving.cache_total_roles_count(total_depriving_roles)

        if total_depriving_roles > constance.config.DEPRIVING_ROLES_WARNING_LIMIT:
            self.log.warning(f'Too many roles was deprived: ({total_depriving_roles})')
            return

        failed_roles = 0
        for group, roles_data in \
                role_data_by_parent_group.items():  # type: depriving.LiteGroup, List[depriving.DeprivingRoleDict]
            role_ids = tuple(role_data['id'] for role_data in roles_data)

            roles = Role.objects.filter(id__in=role_ids)
            try:
                errors = depriving.check_depriving_roles_by_group(roles, data={'slug': group.slug})
            except:
                self.log.exception(f'Error occurred during validate {group.slug} in staff api')
                failed_roles += len(role_ids)
                continue

            if errors:
                self.log.warning(f'Errors during deprived roles from group {group.slug}: {errors}')
                failed_roles += len(role_ids)
                continue

            for chunk in chunkify(role_ids, chunk_size=self.CHUNK_SIZE):
                DepriveDeprivingRoles.delay(depriver_id=None, roles_ids=chunk, block=True)

        depriving.cache_failed_roles_count(failed_roles)


class DepriveDeprivingRoles(BaseTask):
    def init(self, depriver_id, roles_ids, block=False):

        if not roles_ids:
            return

        if depriver_id:
            user = User.objects.get(id=depriver_id)
        else:
            user = User.objects.get_idm_robot()

        lock_name = f'idm.core.tasks.roles.DepriveDeprivingRoles:{user.username}'
        with lock(lock_name, block=block) as acquired:
            if not acquired:
                return

            parent_action = Action.objects.create(
                action=ACTION.MASS_ACTION,
                data={'name': f'deprive_depriving_roles_{user.username}'}
            )

            roles = Role.objects.filter(id__in=roles_ids)
            roles.deprive_or_decline(force_deprive=True,
                                     parent_action=parent_action,
                                     depriver=User.objects.get_idm_robot())


class DeprivePersonalRolesOfDismissedUsers(BaseTask):

    def init(self, system_id=None):
        with lock(f'idm.core.tasks.roles.DeprivePersonalRolesOfDismissedUsers') as acquired:
            if not acquired:
                return

            roles = (
                Role.objects
                .of_operational_system()
                .personal_by_group()
                .filter(
                    state=ROLE_STATE.DEPRIVING_VALIDATION,
                    user__is_active=False,
                )
            )

            if system_id:
                roles = roles.filter(system_id=system_id)

            if not roles:
                return

            parent_action = Action.objects.create(
                action=ACTION.MASS_ACTION,
                data={'name': f'deprive_depriving_roles_personal_dismissed'}
            )

            roles.deprive_or_decline(force_deprive=True,
                                     parent_action=parent_action,
                                     depriver=User.objects.get_idm_robot())


class ExecuteDelayedRoleRequests(BaseTask):

    first_step_by_type = {
        BATCH_REQUEST_TYPE.GRANT: 'request_role',
        BATCH_REQUEST_TYPE.DEPRIVE: 'deprive_role',
    }
    monitor_success = False

    def init(self, batch_request_id=None):
        with lock(f'ExecuteDelayedRoleRequests:{batch_request_id}') as acquired:
            if not acquired:
                return

            delayed_requests = (
                DelayedRoleRequest.objects
                .annotate(type=F('batch_request__type'))
                .filter(is_done=False)
                .values_list('id', 'type', named=True)
            )
            if batch_request_id is None:
                # мониторим только регулярную таску такого типа
                self.monitor_success = True

                delayed_requests = delayed_requests.filter(
                    Q(
                        batch_request__created_at__lte=timezone.now() - timezone.timedelta(minutes=5),
                        is_send=False,
                    ) |
                    Q(
                        batch_request__created_at__lte=timezone.now() - timezone.timedelta(hours=1),
                        is_send=True,
                    )
                )
            else:
                delayed_requests = delayed_requests.filter(batch_request=batch_request_id)

            for request in delayed_requests:
                ExecuteDelayedRoleRequests.delay(
                    step=self.first_step_by_type[request.type],
                    delayed_request_id=request.id,
                )
            delayed_requests.filter(is_send=False).update(is_send=True)

    @atomic_retry
    def request_role(self, delayed_request_id=None):
        from idm.api.v1.forms import DelayedRoleRequestForm
        from idm.api.frontend.rolerequest import extract_kwargs_for_role_request

        with lock(f'ExecuteDelayedRoleRequests:request:{delayed_request_id}') as acquired:
            if not acquired:
                return

            request = (
                DelayedRoleRequest.objects
                .select_related('batch_request')
                .select_for_update(nowait=True, of=('self',))
                .get(id=delayed_request_id)
            )

            if request.is_done:
                return
            request.is_done = True

            form = DelayedRoleRequestForm(request.data)
            if not form.is_valid():
                request.error = form.get_error_message()
                request.save(update_fields=['is_done', 'error'])
                return

            try:
                request.role = Role.objects.request_role(
                    requester=Requester.from_dict(request.batch_request.requester),
                    subject=form.get_subject(),
                    **extract_kwargs_for_role_request(form.cleaned_data),
                )
            except (RoleRequestError, BrokenSystemError, MultiplePassportLoginUsersError, Forbidden) as e:
                request.error = force_text(e)[:255]

            request.save(update_fields=['is_done', 'role', 'error'])

    @atomic_retry
    def deprive_role(self, delayed_request_id=None):
        from idm.api.exceptions import Conflict
        from idm.api.frontend.requesthelpers import convert_role_request_exceptions
        from idm.api.v1.forms import DelayedRoleDepriveForm

        request = (
            DelayedRoleRequest.objects
            .select_related('batch_request', 'role__user')
            .select_for_update(nowait=True, of=('self',))
            .get(id=delayed_request_id)
        )

        if request.is_done:
            return
        request.is_done = True

        form = DelayedRoleDepriveForm(request.data)
        if not form.is_valid():
            request.error = form.get_error_message()
            request.save(update_fields=['is_done', 'error'])
            return

        request.role.lock(for_task=True)
        if request.role.user:
            request.role.user.fetch_department_group()

        requester = Requester.from_dict(request.batch_request.requester)
        try:
            with convert_role_request_exceptions():
                request.role.deprive_or_decline(requester, form.cleaned_data.get('comment'))
        except (Conflict, Forbidden) as exc:
            request.error = force_text(exc)[:255]

        request.save(update_fields=['is_done', 'error'])


DepriveInconsistentRole: celery.Task = app.register_task(DepriveInconsistentRole())
DepriveInconsistentRoleAndRequestNew: celery.Task = app.register_task(DepriveInconsistentRoleAndRequestNew())
DepriveRoleRefs: celery.Task = app.register_task(DepriveRoleRefs())
DepriveRolesWhenDetachingResource: celery.Task = app.register_task(DepriveRolesWhenDetachingResource())
PokeAwaitingRoles: celery.Task = app.register_task(PokeAwaitingRoles())
RoleAdded: celery.Task = app.register_task(RoleAdded())
RoleRemoved: celery.Task = app.register_task(RoleRemoved())
RerunWorkflow: celery.Task = app.register_task(RerunWorkflow())
RequestNewSystemResponsibles: celery.Task = app.register_task(RequestNewSystemResponsibles())
RequestRoleRefs: celery.Task = app.register_task(RequestRoleRefs())
DepriveDeprivingRoles: celery.Task = app.register_task(DepriveDeprivingRoles())
CheckDeprivingRoles: celery.Task = app.register_task(CheckDeprivingRoles())
DeprivePersonalRolesOfDismissedUsers: celery.Task = app.register_task(DeprivePersonalRolesOfDismissedUsers())
ExecuteDelayedRoleRequests: celery.Task = app.register_task(ExecuteDelayedRoleRequests())
