from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.db import models
from django.utils import timezone
from django.utils.encoding import force_text
from django.utils.translation import ugettext, ugettext_lazy as _
from idm.notification.utils import send_notification

from idm.core.querysets.workflow import WorkflowManager
from idm.core.workflow.shortcuts import run_doctests_check
from idm.framework.fields import StrictForeignKey


class Workflow(models.Model):
    STATE_CHOICES = (
        ('edit', _('Правка')),
        ('commited', _('На согласовании')),
        ('approved', _('Согласовано')),
    )
    system = StrictForeignKey('core.System', related_name='workflows', null=False, on_delete=models.CASCADE)
    user = StrictForeignKey('users.User', related_name='workflows', null=False, on_delete=models.CASCADE)
    approver = StrictForeignKey('users.User', null=True, default=None, on_delete=models.CASCADE)
    workflow = models.TextField(blank=True, verbose_name=_('Код пользовательского workflow'))
    group_workflow = models.TextField(blank=True, default='', verbose_name=_('Код группового workflow'))
    comment = models.TextField(blank=True, default='', verbose_name=_('Комментарий'))
    resolution = models.CharField(max_length=255, blank=True, default='')
    added = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now_add=True)
    approved = models.DateTimeField(null=True, default=None, db_index=True)
    state = models.CharField(
        verbose_name=_('Состояние'),
        choices=STATE_CHOICES,
        default='edit',
        max_length=15,
    )
    parent = StrictForeignKey('self', related_name='next', null=True, default=None, on_delete=models.PROTECT)

    objects = WorkflowManager()

    class Meta:
        verbose_name = _('Workflow')
        verbose_name_plural = _('Workflows')
        db_table = 'upravlyator_workflow'

    def edit(self, code, group_code, comment=''):
        if self.state != 'edit':
            raise PermissionDenied(ugettext('Вы не можете редактировать отправленный на согласование workflow.'))
        self.workflow = code
        self.group_workflow = group_code
        self.comment = comment
        self.save()

    def test(self):
        doctest_success, doctest_message = run_doctests_check(self.workflow, self.system)
        group_doctest_success, group_doctest_message = run_doctests_check(
            self.group_workflow, self.system, user_workflow=False)

        if not (doctest_success and group_doctest_success):
            messages = [ugettext('Невозможно сохранить workflow с ошибками:')]
            if not doctest_success:
                messages.append(force_text(doctest_message))
            if not group_doctest_success:
                messages.append(force_text(group_doctest_message))
            raise ValueError('\n'.join(messages))

    def commit(self, committer, send_mail=True):
        if not self.system.is_permitted_for(committer, 'core.idm_view_workflow'):
            raise PermissionDenied(ugettext('Вы не можете редактировать workflow'))

        if self.state != 'edit':
            raise PermissionDenied(ugettext('Вы можете отправлять workflow на подтверждение только из '
                                            'режима редактирования.'))

        actual_wf = self.system.actual_workflow
        if self.workflow == actual_wf.workflow and self.group_workflow == actual_wf.group_workflow:
            raise ValueError(ugettext('Код workflow не отличается от текущего.'))

        self.test()

        self.state = 'commited'
        self.save()
        if send_mail:
            self.send_approve_email()

    def can_approve(self, user):
        if user.has_perm('core.approve_workflow'):
            return True
        if self.system.workflow_approve_policy == 'another' and self.user == user:
            return False
        return self.system.is_permitted_for(user, 'core.approve_workflow')

    def approve(self, approver, send_mail=True, bypass_checks=False):
        from idm.core.models import Action

        if not bypass_checks:
            if not self.system.is_permitted_for(approver, 'core.approve_workflow'):
                raise PermissionDenied(ugettext('У вас нет прав на принятие этого запроса на изменение workflow.'))
            if self.state not in ('edit', 'commited'):
                raise PermissionDenied(ugettext('Нельзя повторно принять запрос на изменение workflow.'))
            if not self.can_approve(approver):
                raise PermissionDenied(ugettext('В этой системе запрещено подтверждать свои изменения.'))

            actual_wf = self.system.actual_workflow
            if actual_wf is None:
                raise ValueError(ugettext('В системе нет текущего workflow'))
            if self.workflow == actual_wf.workflow and self.group_workflow == actual_wf.group_workflow:
                raise ValueError(ugettext('Код workflow не отличается от текущего.'))

            self.test()

        old_workflow_id = self.system.actual_workflow_id
        diff, group_diff = self.get_diff_with_current(highlight=False)

        self.state = 'approved'
        self.approver = approver
        self.approved = timezone.now()
        self.parent_id = old_workflow_id
        self.save()

        self.system.actual_workflow = self
        self.system.save()

        Action.objects.create(
            requester=self.user,
            action='change_workflow',
            system=self.system,
            data=dict(
                new_workflow=self.id,
                old_workflow=old_workflow_id,
            )
        )

        if send_mail:
            context = {
                'system': self.system,
                'user': self.user,
                'diff': diff,
                'group_diff': group_diff
            }
            # Отправляет письмо редактору о принятии его правок workflow
            if approver != self.user:
                send_notification(
                    _('Ваши изменения workflow системы %s применены.') % self.system.get_name(),
                    ['emails/approve_workflow_accepted.txt'],
                    [self.user],
                    context,
                )

            # Отправляет уведомление об изменении workflow пользователем user
            # на рассылку security-root@yandex-team.ru
            send_notification(
                _('Изменение workflow системы %s.') % self.system.get_name(),
                ['emails/service/workflow_changed.txt'],
                settings.EMAILS_FOR_REPORTS,
                context,
            )

    def send_approve_email(self):
        """Отправляет письмо о готовности workflow к подтверждению"""
        diff, group_diff = self.get_diff_with_current(highlight=False)
        context = {
            'system': self.system,
            'workflow': self,
            'user': self.user,
            'diff': diff,
            'group_diff': group_diff,
        }

        # email'ы людей, которые могут аппрувить workflow
        approvers = self.system.get_users_with_permissions('core.approve_workflow')
        if self.system.workflow_approve_policy == 'another':
            approvers = approvers.exclude(id=self.user_id)
        approvers_email = set(approvers.values_list('email', flat=True))
        approvers_email |= set(settings.EMAILS_FOR_REPORTS)

        send_notification(
            _('Изменение workflow системы %s требует подтверждения.') % self.system.get_name(),
            ['emails/approve_workflow.txt'],
            list(sorted(approvers_email)),
            context,
            headers={'Reply-To': self.user.email or ('%s@yandex-team.ru' % self.user.username)},
        )

    def decline(self, approver):
        if self.state not in ('edit', 'commited'):
            raise PermissionDenied(ugettext('Нельзя отклонить утверждённую версию workflow.'))
        if not self.system.is_permitted_for(approver, 'core.approve_workflow'):
            raise PermissionDenied(ugettext('У вас нет прав на отклонение этого запроса на изменение workflow.'))
        self.state = 'edit'
        self.save()

    def get_diff_with_current(self, highlight=False, full=False):
        """Возвращает diff хранимого и принятого в данный момент в системе workflow"""
        return Workflow.objects.get_diffs(self.system.actual_workflow, self, highlight=highlight, full=full)

    def get_doctest(self, requester, subject, node, approvers):
        # формируем пример доктеста, чтобы администратор мог легко
        # встроить его в свой код, описывающий workflow
        doctest = '\n'.join((
            '"""',
            '>>> run(user("%(requester)s"), %(subject)s, %(role)s)',
            '%(result)s',
            '',
            '"""',
        )) % {
            'requester': requester.username,
            'result': repr(approvers),
            'role': '{%s}' % ', '.join("u'{key}': u'{value}'".format(key=key, value=value)
                                       for key, value in node.data.items()),
            'subject': subject.for_doctest,
        }
        return doctest