# coding: utf-8

import logging
import constance

from typing import Tuple, Dict, Set, Any

from django_pgaas import atomic_retry
from django.conf import settings
from django.core.cache import cache
from django.utils import timezone
from django.db import transaction
from django.db.models import Q, F

from idm.celery_app import app
from idm.core.constants.action import ACTION
from idm.core.constants.email_templates import PASSPORT_LOGIN_NEED_ATTACH_NEW_GROUP_ROLE_TEMPLATES
from idm.core.constants.groupmembership_system_relation import (
    MEMBERSHIP_SYSTEM_RELATION_STATE,
    GROUPMEMBERSHIP_INCONSISTENCY,
)
from idm.core.constants.passport_login import CREATED_FLAG_REASON
from idm.core.constants.system import SYSTEM_GROUP_POLICY
from idm.core.exceptions import MultiplePassportLoginsError, PassportLoginGenerationError, PushDisabled
from idm.core.models import System, Action, GroupMembershipSystemRelation, UserPassportLogin, Role
from idm.core.tasks.roles import RegisterPassportLoginMixin
from idm.core.plugins.errors import BasePluginError
from idm.inconsistencies.managers import GroupMembershipInconsistencyQuerySet
from idm.inconsistencies.models import GroupMembershipInconsistency
from idm.framework.task import BaseTask, UnrecoverableError
from idm.utils.actions import start_stop_actions
from idm.users.models import GroupMembership, User
from idm.utils.lock import lock

log = logging.getLogger(__name__)


class PushGroupMembershipSystemRelations(BaseTask):
    monitor_success = False  # таски этого типа посистемные, логгинг будет кастомный

    def get_plugin_method(self, system: System):
        raise NotImplementedError

    @property
    def success_state(self):
        raise NotImplementedError

    @property
    def initial_state(self):
        raise NotImplementedError

    @property
    def success_action_name(self):
        raise NotImplementedError

    @property
    def error_action_name(self):
        raise NotImplementedError

    @property
    def timestamp_name(self):  # название таски для записи времени завершения в SystemMetainfo
        raise NotImplementedError

    def _create_actions(self, sysmembership_ids, action_name, parent):
        actions = [
            Action(action=action_name, sysmembership_id=sysmembership_id, parent=parent)
            for sysmembership_id in sysmembership_ids
        ]
        Action.objects.bulk_create(actions)

    def create_success_actions(self, sysmembership_ids, parent):
        return self._create_actions(sysmembership_ids, self.success_action_name, parent)

    def create_error_actions(self, sysmembership_ids, parent):
        return self._create_actions(sysmembership_ids, self.error_action_name, parent)

    def membership_filter(self):
        return Q(state=self.initial_state)

    def init(self, system_id, batch_size=None, group_id=None, force=False):
        system = System.objects.select_related('metainfo').get(pk=system_id)
        lock_name = 'idm_push_{state}_group_membership_system_relations_for_system_{slug}'.format(
            state=self.initial_state,
            slug=system.slug,
        )

        with lock(lock_name) as acquired:
            if not acquired:
                return

            batch_size = batch_size or system.push_batch_size
            assert batch_size != 0

            self._set_monitor_timestamp(system, 'start')

            filter_ = self.membership_filter()
            if group_id is not None:
                filter_ &= Q(membership__group_id=group_id)

            parent_action = Action(
                action=ACTION.MASS_ACTION,
                data={'name': 'bath_membership_push'},
            )

            last_membership = -1
            while last_membership is not None:
                try:
                    success_ms, error_ms = self.push_batch(
                        system=system,
                        membership_filter=filter_ & Q(pk__gt=last_membership),
                        batch_size=batch_size,
                        action=parent_action,
                        force=force,
                    )
                except PushDisabled:
                    return

                last_membership = max(success_ms | error_ms, default=None)

            self._set_monitor_timestamp(system, 'finish')

    @transaction.atomic
    def push_batch(self, system, membership_filter, batch_size, action, force) -> Tuple[Set[int], Set[int]]:
        """
        Возвращает множества успешно и неудачно отправленных членств
        """
        memberships = list(
            system.sysmemberships
            .filter(membership_filter)
            .values(
                'pk',
                login=F('membership__user__username'),
                group=F('membership__group__external_id'),
                # Логин из мембершипа как минимум потому, что gmsr создается без него
                external_login=F('membership__passport_login__login'),
            )
            .order_by('pk')
            .select_for_update(of=('self',))[:batch_size]
        )

        if not memberships:
            return set(), set()

        login_group_to_membership_id = {
            (membership['login'], membership['group']): membership['pk']
            for membership in memberships
        }
        membership_ids = set(login_group_to_membership_id.values())

        data_to_send = []
        for membership in memberships:
            data_to_send.append({
                'login': membership['login'],
                'group': membership['group'],
            })
            if system.group_policy == SYSTEM_GROUP_POLICY.AWARE_OF_MEMBERSHIPS_WITH_LOGINS:
                data_to_send[-1]['passport_login'] = membership['external_login'] or ''

        if action.id is None:
            action.save()

        try:
            response_data = self.get_plugin_method(system)(data_to_send, request_id=action.id)
        except (BasePluginError, ValueError):
            self.create_error_actions(membership_ids, parent=action)
            return set(), membership_ids

        error_memberships = set()
        error_actions = []
        if response_data['code'] == 207:
            for failed_membership in response_data['multi_status']:
                bad_membership_data = (failed_membership.get('login'), failed_membership.get('group'))
                membership_id = login_group_to_membership_id.get(bad_membership_data)
                if membership_id is None:
                    log.warning('System returned incorrect membership data: %s', bad_membership_data)
                    continue

                error_actions.append(Action(
                    action=self.error_action_name,
                    sysmembership_id=membership_id,
                    error=failed_membership.get('error', '')[:255],
                    parent=action,
                ))

                error_memberships.add(membership_id)
                membership_ids.remove(membership_id)

            Action.objects.bulk_create(error_actions)

        data_for_update = {'need_update': False}
        if self.success_state is not None:
            data_for_update.update({
                'state': self.success_state,
                'updated_at': timezone.now(),
            })

        GroupMembershipSystemRelation.objects.filter(pk__in=membership_ids).update(**data_for_update)
        self.create_success_actions(membership_ids, parent=action)

        return membership_ids, error_memberships

    def _set_monitor_timestamp(self, system: System, stage: str):
        metainfo_field = f'last_{self.timestamp_name}_{stage}'
        setattr(system.metainfo, metainfo_field, timezone.now())
        system.metainfo.save(update_fields=[metainfo_field])


class ActivateGroupMembershipSystemRelations(PushGroupMembershipSystemRelations):
    initial_state = MEMBERSHIP_SYSTEM_RELATION_STATE.ACTIVATING
    success_state = MEMBERSHIP_SYSTEM_RELATION_STATE.ACTIVATED
    success_action_name = ACTION.SYSMEMBERSHIP_ACTIVATE
    error_action_name = ACTION.SYSMEMBERSHIP_ACTIVATION_FAILED
    timestamp_name = 'activate_memberships'

    def get_plugin_method(self, system: System):
        return system.plugin.add_group_membership


class DepriveGroupMembershipSystemRelations(PushGroupMembershipSystemRelations):
    initial_state = MEMBERSHIP_SYSTEM_RELATION_STATE.DEPRIVING
    success_state = MEMBERSHIP_SYSTEM_RELATION_STATE.DEPRIVED
    success_action_name = ACTION.SYSMEMBERSHIP_DEPRIVE
    error_action_name = ACTION.SYSMEMBERSHIP_DEPRIVATION_FAILED
    timestamp_name = 'deprive_memberships'

    def get_plugin_method(self, system: System):
        return system.plugin.remove_group_membership

    def push_batch(self, system, membership_filter, batch_size, action, force) -> Tuple[Set[int], Set[int]]:
        threshold = constance.config.GROUPMEMBERSHIP_SYSTEM_DEPRIVING_THRESHOLD
        memberships_count = system.sysmemberships.filter(membership_filter).count()
        if memberships_count >= threshold and not force:
            raise PushDisabled
        return super().push_batch(system, membership_filter, batch_size, action, force)


class UpdateGroupMembershipSystemRelations(PushGroupMembershipSystemRelations):
    initial_state = None
    success_state = None
    success_action_name = ACTION.SYSMEMBERSHIP_LOGIN_UPDATED
    error_action_name = ACTION.SYSMEMBERSHIP_LOGIN_UPDATE_FAILED
    timestamp_name = 'update_memberships'

    def get_plugin_method(self, system: System):
        return system.plugin.add_group_membership

    def membership_filter(self):
        return Q(state__in=MEMBERSHIP_SYSTEM_RELATION_STATE.PUSHABLE_STATES, need_update=True)


class SyncGroupMembershipSystemRelations(BaseTask):
    monitor_success = False  # таска посистемная, логгинг кастомный

    # К сожалению context-менеджера atomic_retry нет, поэтому пришлось вынести такую фигню в отдельную функцию
    @atomic_retry
    def _create_action_and_update(
            self,
            system: System,
            action_type: str,
            action_data: Any,
            update_qs: GroupMembershipInconsistencyQuerySet = None,
            update_kwargs: Dict[str, Any] = None
    ):
        system.actions.create(
            action=action_type,
            data=action_data,
        )
        if update_qs is not None:
            if update_kwargs is None:
                raise ValueError('update_kwargs')
            update_qs.update(**update_kwargs)

    def push_inconsistencies(self, system, inconsistencies_ids, batch_size, plugin_method):
        for position in range(0, len(inconsistencies_ids), batch_size):
            batch_inconsistencies = GroupMembershipInconsistency.objects.filter(
                pk__in=inconsistencies_ids[position: position + batch_size]
            )
            data_qs = batch_inconsistencies.values_list('username', 'group_id', 'passport_login')
            data = [{
                'login': user,
                'group': group,
                'passport_login': '' if passport_login is None else passport_login,
            } for user, group, passport_login in data_qs]
            try:
                plugin_method(data)
            except:
                self._create_action_and_update(system, ACTION.GROUP_MEMBERSHIP_INCONSISTENCIES_PUSH_FAILED, data)
                system.actions.create(
                    action=ACTION.GROUP_MEMBERSHIP_INCONSISTENCIES_PUSH_FAILED,
                    data=data,
                )
            else:
                self._create_action_and_update(
                    system,
                    ACTION.GROUP_MEMBERSHIP_INCONSISTENCIES_RESOLVED,
                    data,
                    batch_inconsistencies,
                    {
                        'state': GROUPMEMBERSHIP_INCONSISTENCY.STATES.RESOLVED,
                        'updated': timezone.now()
                    }
                )

    @atomic_retry
    def init(self, system_id, check_only=False, resolve_only=False, batch_size=None, requester_id=None):
        system = System.objects.select_related('metainfo').get(pk=system_id)
        if batch_size is None:
            batch_size = system.push_batch_size

        if not resolve_only:
            # Делаем это на в самом шаге, чтобы успеть закоммитить транзакцию отдельно от сверки
            system.metainfo.last_check_memberships_start = timezone.now()
            system.metainfo.save(update_fields=['last_check_memberships_start'])
            nextstep = {
                'step': 'check',
                'system_id': system_id,
                'requester_id': requester_id,
                'check_only': check_only,
                'batch_size': batch_size,
            }
            return nextstep

        nextstep = {
            'step': 'resolve',
            'system_id': system_id,
            'requester_id': requester_id,
            'batch_size': batch_size,
        }
        return nextstep

    @atomic_retry
    def check(self, system_id, check_only, batch_size, requester_id):
        system = System.objects.select_related('metainfo').get(pk=system_id)
        requester = requester_id and User.objects.get(pk=requester_id)
        # Сохраняем last_check_memberships_start в init, чтобы не попасть в транзакцию
        GroupMembershipInconsistency.objects.check_system(system, requester)
        system.metainfo.last_check_memberships_finish = timezone.now()
        system.metainfo.save(update_fields=['last_check_memberships_finish'])
        if not check_only:
            nextstep = {
                'step': 'resolve',
                'system_id': system_id,
                'requester_id': requester_id,
                'batch_size': batch_size,
            }
            return nextstep

    def resolve(self, system_id, batch_size, requester_id):
        system = System.objects.select_related('metainfo').get(pk=system_id)
        requester = requester_id and User.objects.get(pk=requester_id)
        with lock('idm_resolve_group_membership_inconsistencies_for_system_{}'.format(
                system.slug)) as acquired:
            if acquired:
                log.info('Acquired lock in push_group_membership_system_relations')
            else:
                log.info('Cannot acquire lock in push_group_membership_system_relations')
                return

            system.metainfo.last_resolve_memberships_start = timezone.now()
            system.metainfo.save(update_fields=['last_resolve_memberships_start'])

            with start_stop_actions(
                    ACTION.STARTED_MEMBERSHIP_PUSH,
                    ACTION.FINISHED_MEMBERSHIP_PUSH,
                    extra={'requester': requester, 'system': system},
            ):
                system_side = system.groupmembership_inconsistencies.active_system_side_inconsistencies()
                self.push_inconsistencies(
                    system,
                    system_side.values_list('pk', flat=True),
                    batch_size,
                    system.plugin.remove_group_membership,
                )

                our_side = system.groupmembership_inconsistencies.active_our_side_inconsistencies()
                self.push_inconsistencies(
                    system,
                    our_side.values_list('pk', flat=True),
                    batch_size,
                    system.plugin.add_group_membership,
                )

            system.metainfo.last_resolve_memberships_finish = timezone.now()
            system.metainfo.save(update_fields=['last_resolve_memberships_finish'])


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

    def init(self, membership_id, role_id=None, **kwargs):
        membership = GroupMembership.objects.select_related('user').get(pk=membership_id)
        if membership.passport_login:
            self.log.info(
                'Passport login in membership %s already exists', membership_id
            )
            return
        user = membership.user
        try:
            login = user.get_passport_login()
            instance_name, instance_id = ('Role', role_id) if role_id else ('Membership', membership_id)

            self.log.info(
                '%s %s requires passport_login, but it is not added to group, '
                'login %s will be used (create if it is missing)',
                instance_name, instance_id, login,
            )
            nextstep = {
                'step': 'add_passport_login',
                'membership_id': membership_id,
                'passport_login': login,
            }
            return nextstep
        except PassportLoginGenerationError:
            self.log.info(
                'User %s does not have passport_login, all automatically generated already exist, '
                'create a login failed',
                user.username,
            )
        except MultiplePassportLoginsError:
            self.log.info(
                'User %s has several passport_logins, '
                'but none of them is tied to group',
                user.username,
            )
        if role_id:
            nextstep = {
                'step': 'email_about_attach_passport_login',
                'membership_id': membership_id,
                'role_id': role_id,
            }
            return nextstep

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

        membership = GroupMembership.objects.select_related('user').get(pk=membership_id)
        self.init_attributes(membership=membership)
        user = membership.user
        self.passport_login_created = False

        with lock(
                'idm.core.tasks.group_memberships.CheckGroupMembershipPassportLogin.add_passport_login:%s'
                % passport_login, block=True):
            self.check_or_register_login(passport_login)

            with transaction.atomic():
                try:
                    passport_login = UserPassportLogin.objects.add(passport_login, user=user)
                    membership.passport_login = passport_login
                    membership.save(update_fields=['passport_login'])
                    if self.passport_login_created:
                        passport_login.created_by_idm = True
                        passport_login.created_flag_reason = CREATED_FLAG_REASON.EXACT
                        passport_login.save(update_fields=['created_by_idm', 'created_flag_reason'])
                except Exception as err:
                    self.log.exception('Cannot save passport login <%s> state for user <%s>',
                                       passport_login, user.username)
                    raise UnrecoverableError(str(err))

    def email_about_attach_passport_login(self, membership_id, role_id):
        # Этот шаг заускается при выдаче роли с паспортным логином на группу
        # В aware_of_memberships системе. Письма при вступлении в группу отправляются в
        # таске по синку групп, чтобы склеить письма для одного пользователя
        membership = GroupMembership.objects.select_related('group', 'user', 'user__department_group').get(pk=membership_id)
        role = Role.objects.select_related('system', 'node').get(pk=role_id)
        user = membership.user

        context = {
            'group_external_id': membership.group.external_id,
            'group_name': membership.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:
            membership.notified_about_passport_login = True
            membership.save(update_fields=('notified_about_passport_login',))


class SyncAndPushMemberships(BaseTask):
    monitor_success = False

    def init(self, system_id, group_id):
        from idm.users.models import Group

        cache_key = f'SyncAndPushMemberships_{system_id}_{group_id}'
        if cache.get(cache_key):
            log.info('SyncAndPushMemberships was already done for group %s system %s', group_id, system_id)
            return

        cache.set(
            cache_key,
            1,
            settings.IDM_SYNC_AND_PUSH_MEMBERSHIPS_CACHE_TIME,
        )

        system = System.objects.get(id=system_id)
        group = Group.objects.get(id=group_id) if group_id else None
        GroupMembershipSystemRelation.objects.sync_groupmembership_system_relations(system, group)
        system.push_activating_group_memberships_async(group=group)


ActivateGroupMembershipSystemRelations = app.register_task(ActivateGroupMembershipSystemRelations())
CheckGroupMembershipPassportLogin = app.register_task(CheckGroupMembershipPassportLogin())
DepriveGroupMembershipSystemRelations = app.register_task(DepriveGroupMembershipSystemRelations())
SyncAndPushMemberships = app.register_task(SyncAndPushMemberships())
SyncGroupMembershipSystemRelations = app.register_task(SyncGroupMembershipSystemRelations())
UpdateGroupMembershipSystemRelations = app.register_task(UpdateGroupMembershipSystemRelations())
