# coding: utf-8


import doctest
import io
import sys

from django.utils.encoding import force_text
from django_tools_log_context.profiler import execution_profiler
from py.code import Source, Traceback
from ylog.context import log_context

from idm.core.workflow.exceptions import WorkflowError, WorkflowSyntaxError, Return
from idm.core.workflow.plain import context
from idm.core.workflow.plain.user import userify


class PlainWorkflowExecutor(object):
    context_class = context.WorkflowContext

    def __init__(self, code, system=None):
        self.code = force_text(code)
        if 'coding:' not in self.code:
            self.code = '# coding: utf-8\n' + self.code
        self.code = self.code.replace('\r', '').strip()
        self.source = Source(self.code)
        self.code_object = None
        self.context = self.context_class()
        self.system = system

    def compile(self):
        if not self.code_object:
            self.code_object = self.source.compile()

    def run(self, **kwargs):
        self.compile()
        reason = kwargs.get('reason')
        self.context.preprocess(**kwargs)
        system = kwargs['system']
        system_slug = system.slug if system else None

        try:
            with log_context(system=system_slug, reason=reason, log_name='plain_workflow'):
                with execution_profiler(
                    code_block_name='workflow; plain; system %s; reason %s' % (system_slug, reason),
                    code_block_type='plain_workflow',
                    threshold=0
                ):
                    exec(self.code_object, self.context)
        except Return:
            pass
        except WorkflowError:
            # ошибки типа AccessDenied пропускаем выше
            raise
        except Exception:
            _, exception, tb = sys.exc_info()
            traceback = Traceback(tb)
            error_source = traceback.getcrashentry()
            line = force_text(error_source.statement)
            lineno = error_source.lineno
            raise WorkflowSyntaxError(exception, line, lineno, error_source.locals, self.context)

        self.context.postprocess(**kwargs)
        return self.context

    def run_catch_exceptions(self, expected_fields=None, **kwargs):
        try:
            context = self.run(**kwargs)
            if not expected_fields:
                result = context.get('approvers')
            else:
                result = [context.get(field) for field in expected_fields]
        except WorkflowSyntaxError as exception:
            result = exception.verbose_msg()
        return result

    def run_doctests(self, **kwargs):
        """Запускает выполнение доктестов на базе кода workflow.
        Возвращает пару (результат, сообщение), где результат - булево значение, а сообщение - строка
        """
        messages = []
        try:
            self.compile()
        except Exception as exception:
            status = False
            messages.append(str(exception))
        else:
            finder = doctest.DocTestFinder(recurse=False)
            runner = doctest.DocTestRunner(optionflags=doctest.ELLIPSIS)
            status = True

            old_context = self.context
            self.context = old_context.copy()
            self.context.update(**kwargs)
            for test in finder.find(self.code, 'workflow.py', globs=self.context):
                with io.StringIO() as out:
                    test_result = runner.run(test, out=out.write)
                    messages.append(out.getvalue())
                    if test_result.failed > 0:
                        status = False
            self.context = old_context
        return status, '\n'.join(map(force_text, messages))

    def check(self, **kwargs):
        """Функция для тестирования workflow в доктестах.

        Принимает запрашивающего, того для кого запрашивается роль и
        саму роль (в виде словарика).
        Возвращает пару (успех, результат). Успех - булево,
        результат - список аппруверов в случае успешного выполнения, сообщение об ошибке если нет
        """
        kwargs.update({
            'run': self.run_catch_exceptions,
            'user': userify
        })
        status, result = self.run_doctests(**kwargs)

        return status, result


class PlainUserWorkflowExecutor(PlainWorkflowExecutor):
    context_class = context.UserWorkflowContext


class PlainGroupWorkflowExecutor(PlainWorkflowExecutor):
    context_class = context.GroupWorkflowContext
