# coding: utf-8

import collections
import logging
from typing import TYPE_CHECKING

import waffle

if TYPE_CHECKING:
    from idm.core.models import RoleNode, System

from django.conf import settings
from django.core.cache import cache
from django.utils.translation import ugettext_lazy as _

from idm.core.canonical import CanonicalRef
from idm.core.constants.workflow import DEFAULT_PRIORITY, PRIORITY_POLICIES, REQUEST_TYPE, RUN_REASON
from idm.core.workflow.exceptions import (AccessDenied, NoApproversDefined, WorkflowError,
                                          BaseReferenceRolesValidationError, TooManyApproversDefined,
                                          ApproverNotFoundError, Return, WorkflowDismissiedApproverError)
from idm.core.workflow.plain.approver import approverify, AnyApprover, Approver, flatten_approverify, load_users
from idm.core.workflow.plain.conflict import ConflictWrapper
from idm.core.workflow.plain.group import groupify
from idm.core.workflow.plain.node import NodeWrapper
from idm.core.workflow.common.recipient import recipientify, Recipient
from idm.core.workflow.plain.system import SystemWrapper, systemify
from idm.core.workflow.plain.user import userify, all_heads_of, get_head_of, get_head_of_or_zam, try_userify
from idm.notification.utils import send_notification
from idm.utils.human import inflect
from idm.utils.i18n import get_lang_key

log = logging.getLogger(__name__)


class WorkflowContext(dict):
    class WRAPPERS:
        CONFLICT = ConflictWrapper

    builtins = {
        # Python builtins
        'True': True,
        'False': False,
        'None': None,
        # classes
        'approver': Approver,
        'any_from': AnyApprover,
        'AccessDenied': AccessDenied,
        'Return': Return,
        'recipient': Recipient,
        'system': None,
        'REQUEST_TYPE': REQUEST_TYPE,
        'RUN_REASON': RUN_REASON,
        # -ify functions
        'systemify': systemify,
        'userify': userify,
        'try_userify': try_userify,
        'groupify': groupify,
        # other functions
        'all_heads_of': all_heads_of,
        'find_conflicts': ConflictWrapper.find_conflicts_wrapper,
    }
    defaults = {
        'notify_everyone': False,
        'ttl_days': None,
        'review_days': None,  # будет проставлен preprocess()
        'send_sms': False,
        'no_email': False,
    }
    default_lists = (
        'ad_groups',
        'email_cc',
        'ref_roles',
    )

    def __init__(self):
        builtins = {}
        defaults = {}
        for base in reversed(self.__class__.__mro__):
            if hasattr(base, 'builtins'):
                builtins.update(base.builtins)
            if hasattr(base, 'defaults'):
                defaults.update(defaults)
        builtins.update(self.builtins or {})
        defaults.update(self.defaults or {})

        for key in self.default_lists:
            defaults[key] = []

        self.builtins = builtins
        self.defaults = defaults
        super(WorkflowContext, self).__init__()

    def preprocess(self, role_data, system, requester, node, **kwargs):
        self.update(self.builtins)
        self.update(self.defaults)
        self.update(kwargs)
        self['review_days'] = None
        self['role'] = role_data
        self['system'] = SystemWrapper(system)
        self['requester'] = userify(requester)
        self['original_requester'] = userify(kwargs.get('original_requester'))
        self['node'] = NodeWrapper(node)
        if self['requester']:
            self['requester'].context = self

    def postprocess(self, node, system, requester, ignore_approvers=False, **kwargs):
        from idm.core.workflow.common.validation import WorkflowForm

        def validate_conflicts(conflicts):
            if waffle.switch_is_active('find_conflicts_in_workflow'):
                validation_message = _('Неверный формат конфликтов. Ожидаемое значение - результат find_conflicts()')
                if not isinstance(conflicts, dict):
                    raise WorkflowError(validation_message)
                for conflict_set in conflicts.values():
                    if not isinstance(conflict_set, (list, tuple)):
                        raise WorkflowError(validation_message)
                    for item in conflict_set:
                        if not isinstance(item, (list, tuple)) or not len(item) == 2:
                            raise WorkflowError(validation_message)
                        role, conflict = item
                        if not isinstance(conflict, self.WRAPPERS.CONFLICT):
                            raise WorkflowError(validation_message)
            else:
                if not isinstance(conflicts, (list, tuple)):
                    raise WorkflowError(_('Конфликты ролей должны быть представлены в виде списка списков'))

                for rule in conflicts:
                    if not isinstance(rule, (list, tuple)):
                        raise WorkflowError(_('Правило в конфликтах ролей должно быть представлены в виде списка'))

        email_cc = self.get('email_cc')
        if email_cc is None:
            self['email_cc'] = None
        else:
            if isinstance(email_cc, (list, tuple)):
                cc_list = list(map(recipientify, email_cc))
            else:
                cc_list = [recipientify(email_cc)]

            cc_dict = collections.defaultdict(list)
            for recipient in cc_list:
                for state in recipient.states:
                    cc_dict[state].append({
                        'email': recipient.email,
                        'lang': recipient.language,
                        'pass_to_personal': recipient.pass_to_personal
                    })

            self['email_cc'] = dict(cc_dict)

        conflicts = self.get('conflicts')

        if conflicts:
            validate_conflicts(conflicts)
        approvers = self.get('approvers')
        # дальнейшие проверки проверяют approver-ов, которые не нужны в режиме ignore_approvers
        if ignore_approvers:
            return

        if approvers:
            if node and not node.is_public:
                raise AccessDenied(_('Вы не можете запросить скрытую роль с подтверждающими'))

            if isinstance(approvers, AnyApprover):
                # на верхнем уровне не должно быть AnyApprover, но если он там оказался, то считаем, что это
                # единственная OR-группа, случайно попавшая на верхний уровень
                approvers = [approvers]

            approvers = list(map(approverify, approvers))
            flat_approvers = list(flatten_approverify(approvers))
            approvers_number = len(flat_approvers)
            if approvers_number > system.max_approvers:
                log.warning(
                    'Role %s in system %s has too many approvers (%s > %s), requester=%s',
                    node.value_path,
                    system.slug,
                    approvers_number,
                    system.max_approvers,
                    requester,
                )
                raise TooManyApproversDefined(_(
                        'Для роли определено слишком большое количество подтверждающих (сейчас: {}, разрешено: {}). '
                        'Для решения проблемы напишите на tools@.').format(
                        approvers_number,
                        system.max_approvers,
                    )
                )

            try:
                load_users(flat_approvers)
            except ApproverNotFoundError as e:
                cache_key = f'{settings.IDM_NOT_FOUND_USER_CACHE_KEY}_{system.slug}'
                if not cache.get(cache_key):
                    send_notification(
                        subject=f'В системе {system.name} в списке подтверждающих роль есть несуществующие '
                                f'пользователи',
                        message_templates=['emails/service/workflow_not_found_users.txt'],
                        recipients=system.get_emails(fallback_to_reponsibles=True) + list(
                            settings.EMAILS_FOR_REPORTS),
                        context={
                            'system': system,
                            'username': e.username,
                        }
                    )
                    cache.set(cache_key, 1, timeout=settings.IDM_NOT_FOUND_USER_CACHE_TTL)
                raise

            # создаем новый список аппруверов по правилам:
            # * одиночные аппруверы не должны повторяться
            # * если OR-группа содержит единоличного аппрувера, то она исключается из рассмотрения
            individual_approvers = {approver for approver in approvers if len(approver) == 1}
            new_approvers = []
            seen_individual_approvers = set()
            for approver in approvers:  # снова перебираем старых аппруверов для сохранения порядка
                if len(approver) == 1:
                    # для одиночных аппруверов контролируем единственность
                    if approver.user.username not in seen_individual_approvers:
                        new_approvers.append(approver)
                        seen_individual_approvers.add(approver.user.username)
                else:
                    # если в OR-группе аппруверов содержится единоличный аппрувер,
                    # то эта OR-группа целиком выключается
                    if (approver and
                            all([individual_approver not in individual_approvers for individual_approver in approver])):
                        new_approvers.append(approver)

            for or_group in new_approvers:
                priority = 1
                # если у всех в OR-группе приоритет равен DEFAULT_PRIORITY, то ставить им приоритет слева направо
                if all(approver.priority == DEFAULT_PRIORITY for approver in or_group):
                    for approver in or_group:
                        approver.priority = priority
                        priority += 1

            self['approvers'] = new_approvers
            self.check_approvers(node, system)
        if approvers is None:
            raise NoApproversDefined(_('В workflow не определены подтверждающие (approvers)'))

        workflow_form = WorkflowForm(self)

        if not workflow_form.is_valid():
            raise WorkflowError(workflow_form.get_error_message())

        warnings = self.get_warnings(workflow_form.cleaned_data['warnings'])
        self['warnings'] = []
        if requester:
            lang_key = get_lang_key(default=requester.lang_ui)
            self['warnings'] = [warning[lang_key] for warning in warnings]

        workflow_comment = self.get('workflow_comment')
        if workflow_comment and not isinstance(workflow_comment, str):
            raise WorkflowError(_('workflow_comment должен быть строкой'))

    def check_approvers(self, node: 'RoleNode', system: 'System'):
        """
        Проверяет подтверждающих на наличие уволенных сотрудников. Если хотя бы одна or-группа полностью состоит из
        уволенных сотрудников, то поднимаем WorkflowError, иначе если есть хотя бы один уволенный сотрудник, то
        оповещаем об этом ответственных
        """
        not_active_users = []
        approvers_groups = self['approvers']
        if self.get('request_type') == REQUEST_TYPE.DEPRIVE:
            approvers_groups = [list(flatten_approverify(approvers_groups))]
        for approvers_group in approvers_groups:
            group_is_not_active = True
            group_not_active_users = []
            for approver in approvers_group:
                if approver.user.is_active:
                    group_is_not_active = False
                else:
                    group_not_active_users.append(approver.user)
            not_active_users.extend(group_not_active_users)
            if group_is_not_active:
                raise WorkflowDismissiedApproverError(
                    node.name,
                    self.get('request_type', REQUEST_TYPE.REQUEST),
                    ", ".join([user.username for user in group_not_active_users]))
        if not_active_users:
            # Отправляем письмо ответственным
            not_active_usernames = (x.username for x in not_active_users)
            cache_key = f'{settings.IDM_NOT_ACTIVE_USERS_CACHE_KEY}_{system.slug}_{sorted(not_active_usernames)}'
            if not cache.get(cache_key):
                word = 'подтверждающих' if self.get('request_type') == REQUEST_TYPE.DEPRIVE else 'отзывающих'
                send_notification(
                    subject=f'Уволенные сотрудники в списке {word} роль в системе {system.name}',
                    message_templates=['emails/service/workflow_fired_users.txt'],
                    recipients=system.get_emails(fallback_to_reponsibles=True) + list(settings.EMAILS_FOR_REPORTS),
                    context={
                        'system': system,
                        'users': not_active_users,
                    }
                )
                # Кладем в кэш пару (система, уволенные пользователи)
                cache.set(
                    cache_key,
                    1,
                    timeout=settings.IDM_NOT_ACTIVE_USERS_CACHE_TTL
                )

    def is_auto_approved(self, requester, subject, already_approved=None):
        from idm.core.workflow.common.subject import subjectify
        already_approved = already_approved or set()
        approvers = self['approvers']
        results = []
        for or_group in approvers:
            if not or_group:
                continue
            # в or-group лежат объекты Approver
            or_users = set(approver.user for approver in or_group)

            local_result = any((subject == subjectify(userify(user)) or user in already_approved
                                for user in or_users))

            if not local_result and requester is not None:
                local_result = requester in or_users
            results.append(local_result)
        result = not results or all(results)  # либо пустые подтверждающие, либо все True
        return result

    def get_warnings(self, warnings):
        for ref_role in self['ref_roles']:
            as_canonical = CanonicalRef.from_dict(ref_role)
            try:
                as_canonical.validate()
            except BaseReferenceRolesValidationError:
                warnings.append({
                    'ru': ('В связи с тем, что workflow системы {system} содержит ошибки, связанная роль "{role}" '
                           'не будет выдана. После исправления workflow потребуется перезапрос роли.').format(
                        system=self['system'].get_name(),
                        role=ref_role,
                    ),
                    'en': ('Reference role "{role}" could not be requested due to errors in workflow of '
                           'system {system}. Role will require a rerequest when the workflow is fixed.').format(
                        system=self['system'].get_name(),
                        role=ref_role,
                    )
                })

        return warnings


class UserContextMixin(object):
    def preprocess(self, role_data, system, requester, node, user, **kwargs):
        super(UserContextMixin, self).preprocess(role_data, system, requester, node, **kwargs)
        self['user'] = userify(user)
        self['user'].context = self

    def get_warnings(self, warnings):
        warnings = super(UserContextMixin, self).get_warnings(warnings)

        if not self['requester']:
            return warnings

        self['node'].fetch_system()
        roles = (
            self['user'].get_all_roles(self['node'].object, self.get('fields_data')).
            select_related('group', 'parent__group')
        )

        if not roles.exists():
            return warnings

        groups = set()
        for role in roles:
            if role.group:
                groups.add(role.group)
            elif role.parent and role.parent.group:
                groups.add(role.parent.group)

        warnings.append({
            'ru': 'Вы запрашиваете роль, которая уже выдана {} на основе следующих групп: {}'.format(
                inflect('кому', self['user'].get_full_name('ru'), fio=True),
                ', '.join(group.get_name('ru') for group in groups),
            ),
            'en': 'You are requesting a role that has already been granted for the following groups of {}: {}'.format(
                self['user'].get_full_name('en'),
                ', '.join(group.get_name('en') for group in groups)
            )
        })
        return warnings


class UserWorkflowContext(UserContextMixin, WorkflowContext):
    builtins = {
        'PRIORITY_POLICIES': PRIORITY_POLICIES,
        'get_head_of_or_zam': get_head_of_or_zam,
        'get_head_of': get_head_of,
    }


class GroupContextMixin(object):
    def preprocess(self, role_data, system, requester, node, group, **kwargs):
        super(GroupContextMixin, self).preprocess(role_data, system, requester, node, **kwargs)
        self['group'] = groupify(group)
        self['group'].context = self
        self['inheritance_settings'] = kwargs.get('inheritance_settings', {})

    def get_warnings(self, warnings):
        warnings = super(GroupContextMixin, self).get_warnings(warnings)

        if not self['requester']:
            return warnings

        self['node'].fetch_system()
        roles = (
            self['group'].object.get_roles(
                self['node'].object,
                self.get('fields_data'),
                include_ancestors=True
            ).select_related(
                'group',
            )
        )

        if not roles.exists():
            return warnings

        warnings.append({
            'ru': 'Вы запрашиваете роль, которая уже выдана следующим родителям группы {}: {}'.format(
                self['group'].get_name('ru'),
                ', '.join(role.group.get_name('ru') for role in roles),
            ),
            'en': 'You are requesting a role that has already been granted to the following parents of {}: {}'.format(
                self['group'].get_name('en'),
                ', '.join(role.group.get_name('en') for role in roles)
            )
        })
        return warnings


class GroupWorkflowContext(GroupContextMixin, WorkflowContext):
    pass
