#coding:utf-8
"""
Основные объекты библиотеки: механизмы проверки прав, экшены, паки, контроллеры, кэш контроллеров
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
"""
from functools import wraps
from logging import getLogger
import abc
import inspect
import importlib
import threading
import re
import warnings

from django import http
from django.conf import settings


from results import (
    ActionResult,
    PreJsonResult,
    JsonResult,
    HttpReadyResult,
    TextResult,
    XMLResult,
    BaseContextedResult,
    OperationResult,
    ActionRedirectResult
)

from exceptions import (
    ApplicationLogicException,
    ActionException,
    ActionNotFoundException,
    ActionPackNotFoundException,
    ReinitException,
    ActionUrlIsNotDefined
)

import utils

from context import (
    ActionContext,
    ActionContextDeclaration,
    DeclarativeActionContext,
    RequiredFailed,
    ContextBuildingError,
    CriticalContextBuildingError,
)

from metrics import create_statsd_client


logger = getLogger('django')


_STATSD_CLIENT = create_statsd_client(settings)


ACD = ActionContextDeclaration


_clean_url = lambda s: re.sub(r'^[/^]*(.*?)[$/]*$', r'\1', s)


def _import_by_path(path):
    """
    Импортирует объект по пути @path вида "full.package.path.to.some_object"
    """
    match = re.match(r'^(?:(.*)\.)?([^.]+)$', path)
    if match is None:
        raise ValueError(u'Wrong path to import: %r' % path)
    module_name, obj_name = match.groups()
    module = importlib.import_module(module_name)
    return getattr(module, obj_name)


def _name_of(obj):
    """
    Возвращает для указанного объекта имя вида
    "package.ClassOfObject"
    или
    "package.ClassOfParent.ClassOfObject", если obj имеет атрибут "parent"
    """

    def cls_name(obj):
        return (obj if inspect.isclass(obj) else obj.__class__).__name__

    parent = getattr(obj, 'parent', None)
    if parent:
        parent = '%s.' % cls_name(parent)
    else:
        parent = ''

    package = inspect.getmodule(obj).__name__

    obj = cls_name(obj)

    return '%s.%s%s' % (package, parent, obj)


def _cached_to(attr_name):
    """
    Оборачивает простые методы (без аргументов) и property getters,
    с целью закэшировать первый полученный результат
    """
    def wrapper(fn):
        @wraps(fn)
        def inner(self):
            if hasattr(self, attr_name):
                result = getattr(self, attr_name)
            else:
                result = fn(self)
                setattr(self, attr_name, result)
            return result
        return inner
    return wrapper


def _must_be_replaced_by(alternative):
    """
    Оборачивает устаревший метод таким образом,
    что при вызове метода выводится предупреждение
    с рекомендацией замены метода на текущую альтернативу
    """
    def wrapper(fn):
        @wraps(fn)
        def inner(self, *args, **kwargs):
            class_name = self.__class__.__name__
            # if '%' in alternative:
            #     alternative = alternative % class_name
            warnings.warn(
                "'{clazz}.{method}' deprecated! Use '{alternative}'!".format(
                    clazz=class_name,
                    method=fn.func_name,
                    alternative=alternative
                ), FutureWarning, 2)
            return fn(self, *args, **kwargs)
        return inner
    return wrapper


#==============================================================================
# Абстрактный механизм проверки прав и его реализация,
# проверяющая права через механизм django.contrib.auth.models.User.has_perm
#==============================================================================


class AbstractPermissionChecker(object):
    """
    Абстрактный механизм проверки прав
    """
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def has_action_permission(self, request, action, subpermission=None):
        """
        Метод должен возвращать True, если выполнение экшена
        допустимо в контексте запроса

        :param request: запрос
        :type request: django.http.Request

        :param action: Экшен, наличие прав на выполнение которого проверяется
        :type action: m3_core.actions.Action

        :return: Допустимость выполнения экшена в контесте запроса
        :rtype: True или False
        """
        pass

    @abc.abstractmethod
    def has_pack_permission(self, request, pack, permission):
        """
        Метод должен возвращать True, если действие,
        характеризуемое парой pack/permission,
        допустимо в контексте запроса

        :param request: запрос
        :type request: django.http.Request

        :param pack: пак, для которого возвращается код доступа
        :type pack: m3_core.actions.ActionPack

        :return: Допустимость пака в контесте запроса
        :rtype: True или False
        """
        pass

    @staticmethod
    def get_perm_code(action_or_pack, subpermission=None):
        '''
        Возвращает код действия, для контроля прав доступа

        :param action_or_pack: экшен или пак, для которого
            возвращается код доступа
        :type action_or_pack: m3_core.actions.Action либо
            m3_core.actions.ActionPack

        :param basestring subpermission: код подправа

        :return: код действия для контроля прав доступа
        :rtype: basestring
        '''
        code = _name_of(action_or_pack)
        # уберем из кода префикс системы,
        # т.к. код права должен быть относительным
        if hasattr(settings, 'ROOT_URL') and (
            code.startswith(settings.ROOT_URL)
        ):
            code = code[len(settings.ROOT_URL):]
        if subpermission:
            code = '%s#%s' % (code, subpermission)
        return code


class AuthUserPermissionChecker(AbstractPermissionChecker):
    """
    Backend, проверяющий права через
    django.contrib.auth.models.User.has_perm

    Требует подключения в проекте session-middleware, т.к. опирается
    на наличие в request атрибута 'user',
    указывающего на текущего пользователя
    """
    def has_action_permission(self, request, action, subpermission=None):

        # контракт
        assert isinstance(action, Action)
        assert isinstance(action.parent, ActionPack)
        if subpermission:
            assert isinstance(subpermission, str)

        # если ползователя нет (аноним) - прав тоже нет
        if not request.user:
            return False

        result = True
        if action.need_check_permission:
            if subpermission:
                assert (
                    subpermission in action.sub_permissions or
                    subpermission in action.parent.sub_permissions
                ), (
                    u'Подправо ("%s") должно быть описано в экшне или паке'
                    % subpermission
                )
                result = request.user.has_perm(
                    action.get_perm_code(subpermission))
            else:
                result = request.user.has_perm(action.get_perm_code())
        return result

    def has_pack_permission(self, request, pack, permission):
        assert isinstance(pack, ActionPack)
        if permission:
            assert isinstance(permission, str)

        result = True
        if pack.need_check_permission:
            assert permission in pack.sub_permissions, (
                u'Подправо ("%s") должно быть описано в паке' % permission)
            result = request.user.has_perm(pack.get_perm_code(permission))
        return result


class BypassPermissionChecker(AbstractPermissionChecker):
    """
    Механизм проверки прав, "разрешающий всем и всё"
    """
    def has_action_permission(self, request, action, subpermission=None):
        return True

    def has_pack_permission(self, request, pack, permission):
        return True


class LegacyPermissionChecker(AbstractPermissionChecker):
    """
    Мех-м проверки прав, созданный для совместимости со старыми проектами
    """
    @staticmethod
    def get_perm_code(pack_or_action, subpermission=None):
        code = pack_or_action.get_absolute_url()
        # уберем из кода префикс системы,
        # т.к. код права должен быть относительным
        if hasattr(settings, 'ROOT_URL') and (
                code.startswith(settings.ROOT_URL)):
            code = code[len(settings.ROOT_URL):]
        if subpermission:
            code = '%s#%s' % (code, subpermission)
        return code

    def has_action_permission(self, request, action, subpermission=None):
        '''
        Проверка пака на выполнение действия для указанного пользователя
        '''
        assert isinstance(action.parent, ActionPack)
        # Если в наборе действий need_check_permission=True,
        # а в самом действии False, то права не проверяются
        # Если в наборе действий need_check_permission=True,
        # и в самом действии True, то права проверяются
        # Если в наборе действий need_check_permission=False,
        # а в самом действии True, то права не проверяются
        # Если в наборе действий need_check_permission=False,
        # а в самом действии False, то права не проверяются
        # Т.е. права проверяются только,
        # если в наборе действий и в действии включена проверка прав
        # Признак need_check_permission в вышестоящих наборах действий
        # не влияет на решение, т.к. в тех наборах свои действия

        # Проверим, что в действии и наборе разрешена проверка прав
        user_obj = request.user
        code = self.get_perm_code(action, subpermission)
        if action.need_check_permission and (
                action.parent.need_check_permission):
            # если пользователя нет, значит аноним - ему дадим отпор
            if user_obj:
                # проверим что права на выполнение есть
                return user_obj.has_perm(code)
            else:
                return False
        else:
            return True

    def has_pack_permission(self, request, pack, permission):
        '''
        Проверка на внутреннее право пака для указанного пользователя
        '''
        assert isinstance(permission, str)
        # Подчиненные права набора действий
        # проверяются только в случае разрешения проверки в наборе действий
        # Если переданный код не прописан в правах этого действия,
        # то это не наш код - значит всё разрешено
        user_obj = request.user
        if pack.need_check_permission and permission in pack.sub_permissions:
            # если пользователя нет, значит аноним - ему дадим отпор
            if user_obj:
                # проверим что права на выполнение есть
                return user_obj.has_perm(self.get_perm_code(pack, permission))
            else:
                return False
        return True


class LazyContainer(object):
    """
    Ленивая обёртка для объектов, указываемых в settings.
    Предназначена для позднеё загрузки объектов - по первому обращению
    """

    def __init__(self, fabric):
        self._fabric = fabric

    def _get_obj(self):
        if not hasattr(self, '_cache'):
            self._cache = self._fabric()
        return self._cache

    def __getattr__(self, attr):
        if attr == '_cache':
            return super(LazyContainer, self).__getattribute__(attr)
        else:
            return getattr(self._get_obj(), attr)


def _permission_checker_fabric():
    """
    Получение экземпляра permission checker
    """
    _OLD_PERMISSION_CHECKING = getattr(
        settings, 'CONTROLLER_SHOULD_USE_OLD_PERMISSION_CHECKING', False)

    if _OLD_PERMISSION_CHECKING:
        # backend проверки прав, совместимый со старыми проектами
        result = LegacyPermissionChecker
    else:
        path = getattr(settings, 'M3_PERMISSION_CHECKER', None)
        if path is None:
            # если backend не задан в настройках, то исходим из того,
            # подключены ли пользователи Django
            if 'django.contrib.auth' in settings.INSTALLED_APPS:
                result = AuthUserPermissionChecker
            else:
                result = BypassPermissionChecker
        else:
            # в настройках можно указать свой backend для проверки прав
            if isinstance(path, str):
                # указан строкой - импортируем
                result = _import_by_path(path)
            if not (inspect.isclass(result) and issubclass(
                    result, AbstractPermissionChecker)):
                raise TypeError(
                    u'M3_PERMISSION_CHECKER option value must be '
                    u'a string or class!'
                )
    # бэкенд инстанцируется для дальнейшего использования
    return result()


# ленивый экземпляр бэкенда проверки прав
_permission_checker = LazyContainer(_permission_checker_fabric)


#========================== ИСКЛЮЧЕНИЯ ========================================

#==============================================================================
class Action(object):
    u"""
    Базовый класс, от которого должны наследоваться все Action'ы в системе.
    Заменяет собой классические вьюшки Django.
    """

    #: Часть адреса запроса которая однозначно определяет его принадлежность к
    #: конкретному Action'у
    url = ''

    #: Ссылка на ActionPack к которому принадлежит данный Action
    parent = None

    #: Ссылка на контроллер к которому принадлежит данный Action
    controller = None

    #: Наименование действия для отображения
    verbose_name = None

    #: Признак обработки прав доступа,
    #: при выполнении действия (по-умолчанию отключен)
    #: Как обрабатывается этот признак - смотри в has_permission
    need_check_permission = False

    #: Словарь внутренних прав доступа, используемых в действии
    #: ключ - код права, который совмещается с кодом действия
    #: значение - наименование права
    #: Пример: {'tab2':u'Редактирование вкладки Доп. сведения',
    #: 'work_visible':u'Просмотр сведений о работе'}
    #: Общий код права доступа будет иметь вид:
    #: /edit#tab2 и /edit#work_visible соответственно
    #: Как обрабатывается этот список - смотри в has_sub_permission
    sub_permissions = {}

    #: Логический путь действия в прикладной системе.
    #: Используется только для отображения и группировки
    #: действий с одинаковым путем.
    #: Также может использоваться для создания меню.
    #: Например, путь может быть: "Справочники\Общие" или "Реестры"
    path = None

    @_must_be_replaced_by('%s.get_perm_code')
    def get_sub_permission_code(self, sub_code):
        # метод оставлен для совместимости
        return self.get_perm_code(sub_code)

    @_must_be_replaced_by('%s.has_perm')
    def has_sub_permission(self, user_obj, sub_code, request):
        # метод оставлен для совместимости
        return self.has_perm(request, sub_code)

    @_must_be_replaced_by('%s.get_perm_code')
    def get_permission_code(self):
        # метод оставлен для совместимости
        return self.get_perm_code()

    @_must_be_replaced_by('%s.has_perm')
    def has_permission(self, user_obj, request):
        # метод оставлен для совместимости
        return self.has_perm(request)

    #================= НОВЫЙ ИНТЕРФЕЙС ПРОВЕРКИ ПРАВ ==========================
    def has_perm(self, request, subpermission=None):
        """
        Интерфейсный метод проверки (под)прав экшна.

        Использовать ВМЕСТО has_permission/has_sub_permission

        :param request: запрос
        :type request: django.http.Request
        """
        return _permission_checker.has_action_permission(
            request, self, subpermission)

    def get_perm_code(self, subpermission=None):
        """
        Получение кода (под)права
        """
        # по-умолчанию код генерится backend`ом
        return _permission_checker.get_perm_code(self, subpermission)
    #==========================================================================

    def pre_run(self, request, context):
        """
        Метод для предварительной обработки
        входящего запроса и контекста,
        перед передачений в run().
        Если возвращает значение отличное от None,
        обработка запроса прекращается и результат
        уходит во вьюшку контроллера.

        :param request: запрос
        :type request: django.http.Request

        :param context: Контекст выполнения операции, восстанавливаемый
            из контекств
        :type context: m3_core.actions.context.ActionContext

        :return: None либо объект для обработке во вьюшке контроллера
        """
        pass

    def post_run(self, request, context, response):
        """
        Метод для постобработка результата работы экшенаа.

        :param request: запрос
        :type request: django.http.Request

        :param context: Контекст выполнения операции,
            востанавливаемый из контекств
        :type context: m3_core.actions.context.ActionContext

        :param response: результат выполнения экшена
        :type response: наследник m3_core.actions.results.ActionResult
        """
        pass

    def context_declaration(self):
        """
        Метод объявления необходимости наличия
        определенных параметров в контексте.

        Должен возвращать список из экземпляров *ActionContextDeclaration*
        либо
        словарь описания контекста для *DeclarativeActionContext*

        :return: описание необходимости наличия определенных параметров
            в запросе
        :rtype: list of m3_core.actions.context.ActionContextDeclaration либо
            m3_core.actions.context.DeclarativeActionContext
        """
        pass

    def run(self, request, context):
        """
        Обеспечивает непосредственное исполнение запроса
        (аналог views в Django).
        Обязательно должен быть перекрыт в наследнике.

        :param request: запрос
        :type request: django.http.Request

        :param context: Контекст выполнения операции,
            востанавливаемый из контекств
        :type context: m3_core.actions.context.ActionContext

        :return: результат выполнения экшена
        :rtype: наследник m3_core.actions.results.ActionResult
        """
        raise NotImplementedError()

    @classmethod
    def absolute_url(cls):
        '''
        Возвращает полный путь до действия.
        НО при условии что этот экшен используется
        ТОЛЬКО В ОДНОМ ПАКЕ И КОНТРОЛЛЕРЕ, иначе валим всех!
        Ищет перебором!

        :return: полный путь до действия
        :rtype: basestring
        '''
        url = ControllerCache.get_action_url(cls)
        if not url:
            raise ActionNotFoundException(clazz=cls)
        return url

    def get_packs_url(self):
        """
        Возвращает строку, полный адрес от контроллера до текущего экшена

        :return: полный адрес от контроллера до текущего экшена
        :rtype: basestring
        """
        assert isinstance(self.parent, ActionPack)
        path = []
        pack = self.parent
        while pack is not None:
            path.append(pack.url)
            pack = pack.parent
        return ''.join(reversed(path))

    def get_absolute_url(self):
        '''
        Возвращает полный путь от хоста до конечного экшена
        @deprecated: Дублирует absolute_url
        '''
        #TODO: Переписать, т.к. этот код дублирует функции контроллера
        assert isinstance(self.controller, ActionController), (
            '%s is not actioncontroller in %s' % (self.controller, self))
        # Очищаем от мусора рег. выр.
        url = self.url.replace('$', '')
        return self.controller.url + self.get_packs_url() + url

    @classmethod
    def get_verbose_name(cls):
        return cls.verbose_name if cls.verbose_name else cls.get_short_name()

    @classmethod
    def get_short_name(cls):
        return _name_of(cls)


class ActionPack(object):
    '''
    Базовый класс для всех ActionPack'ов.
    Предназначен для хранения в себе других
    экшенов и паков, схожих по целям.
    '''
    url = ''

    # Адрес экшенпака
    @classmethod
    def get_short_name(cls):
        """
        Имя пака для поиска в ControllerCache

        :return: Имя пака
        :rtype: basestring
        """
        name = cls.__dict__.get('_auto_short_name')
        if not name:
            name = cls._auto_short_name = _name_of(cls)
        return name

    @property
    @_cached_to('__cached_short_name')
    def short_name(self):
        # имя пака для поиска в ControllerCache в виде атрибута
        # для совместимости с m3
        return self.get_short_name()

    @classmethod
    def absolute_url(cls):
        """
        Возвращает полный адрес (url) от контроллера до текущего экшенпака
        """
        path = [cls.url]
        pack = cls.parent
        while pack is not None:
            path.append(pack.url)
            pack = pack.parent
        url = ''.join(reversed(path))

        contr_url = ''
        for cont in ControllerCache.get_controllers():
            p = cont.find_pack(cls)
            if p:
                contr_url = cont.url
                break
        return contr_url + url

    #: Ссылка на вышестоящий пакет, тот в котором зарегистрирован данный пакет
    parent = None

    #: Ссылка на родительский ActionController
    controller = None

    #: Наименование Набора действий для отображения
    verbose_name = None

    #: Признак обработки прав доступа,
    #: при выполнении дочерних действий (по-умолчанию отключен)
    #: Как обрабатывается этот признак - смотри в Action.has_permission
    need_check_permission = False

    #: Словарь внутренних прав доступа, используемых в наборе действий
    #: ключ - код права, который совмещается с адресом (кодом) набора действий
    #: значение - наименование права
    #: Пример: {'edit':u'Редактирование записи'}
    #: Общий код права доступа будет иметь вид: /users#edit
    #: Как обрабатывается этот список - смотри в has_sub_permission
    sub_permissions = {}

    #: Логический путь набора действий в прикладной системе.
    #: Используется только для отображения
    #: и группировке наборов с одинаковым путем.
    #: Также может использоваться для создания меню.
    #: Например, путь может быть: "Справочники\Общие" или "Реестры"
    path = None

    def __init__(self):
        #: Список действий зарегистрированных на исполнение в данном пакете
        self.actions = []
        #: Список дочерних пакетов (подпакетов),
        #: зарегистрированных на исполнение в данном пакете
        self.subpacks = []

    def get_absolute_url(self):
        """
        Возвращает абсолютный путь (НОРМАЛЬНО, в отличие от absolute_url)
        """
        assert self.controller
        pack = self
        if pack.parent:
            path = []
            while pack.parent:
                path.insert(0, pack.url)
                pack = pack.parent
        else:
            path = [pack.url]
        return '/'.join([pack.controller.url] + map(_clean_url, path))

    def get_permission_code(self):
        # метод оставлен для совместимости
        return self.get_perm_code()

    def get_sub_permission_code(self, sub_code):
        # метод оставлен для совместимости
        return self.get_perm_code(sub_code)

    def has_sub_permission(self, user_obj, sub_code, request):
        # метод оставлен для совместимости

        return self.has_perm(request, sub_code)

    #================= НОВЫЙ ИНТЕРФЕЙС ПРОВЕРКИ ПРАВ ==========================
    def has_perm(self, request, permission):
        """
        Интерфейсный метод проверки подправ пака.

        Использовать ВМЕСТО has_sub_permission

        :param request: запрос
        :type request: django.http.Request
        """
        return _permission_checker.has_pack_permission(
            request, self, permission)

    def get_perm_code(self, permission=None):
        """
        Получение кода (под)права
        """
        # по-умолчанию код генерится backend`ом
        return _permission_checker.get_perm_code(self, permission)
    #==========================================================================

    def pre_run(self, request, context):
        """
        Метод для предварительной обработки
        входящего запроса и контекста
        перед передачений в нижестоящий экшен или пак.
        Если возвращает значение отличное от None, обработка запроса
        прекращается и результат уходит во вьюшку контроллера.

        :param request: запрос
        :type request: django.http.Request

        :param context: Контекст выполнения операции, восстанавливаемый
            из контекств
        :type context: m3_core.actions.context.ActionContext
        """
        pass

    def post_run(self, request, context, response):
        '''
        Метод для постобработки результата работы вышестоящего экшена или пака.
        Принимает исходный запрос *request*, результат работы *response* и
        извлеченный контекст *context*.

        :param request: запрос
        :type request: django.http.Request

        :param context: Контекст выполнения операции, восстанавливаемый
            из контекств
        :type context: m3_core.actions.context.ActionContext

        :param response: ответ, полученный в результате выполнения
            действия
        :type response: m3_core.actions.results.ActionResult
        '''
        pass

    @classmethod
    def get_verbose_name(cls):
        """
        Получение понятного имени пака

        :rtype: unicode
        """
        return (
            cls.title if hasattr(cls, 'title') and cls.title
            else cls.verbose_name if cls.verbose_name else cls.__name__
        )


class ActionController(object):
    '''
    Класс коонтроллер - обеспечивает обработку
    пользовательских запросов путем передачи
    их на исполнение соответствущим Action'ам
    '''
    #: Наименование Контроллера для отображения
    verbose_name = None

    class FakePacks:
        """
        Класс содержит заглушки методов, чтобы инспектор кода не ругался,
        т.к. настоящие методы присваиваются классу в рантайме.
        """
        def append(self, item):
            pass

        def extend(self, items_list):
            pass

    def __init__(self, url="", name=None):
        '''
        :param basetring url: используется для отсечения лишней части пути в
            запросе, поскольку управление в пак передается из вьюшки
        :param basestring name: человеческое название контроллера.
            Используется для отладки.
        '''
        #: ДЛЯ СОВМЕСТИМОСТИ.
        #: Имитирует список паков торчащий наружу
        self.packs = self.FakePacks()
        self.packs.append = self.append_pack
        self.packs.extend = self.extend_packs

        # Используется для отсечения лишней части пути в запросе
        self.url = url
        self.name = name

        # Словарь для быстрого разрешения запросов.
        # Состоит из полного пути запроса, списка
        # вызываемых паков и экшена.
        # Пример: {'/dict/lpu/get_rows': ([DictPack, LPUPack], RowsAction)}
        self._url_patterns = {}

        # Словари для быстрого поиска паков по имени и классу, например:
        # {'DictPack', <DictPack instance at 0x01FBACB0>}
        self._packs_by_name = {}
        self._packs_by_type = {}
        self._actions_by_type = {}
        self._actions_by_name = {}
        self._nodes_by_perm = {}
        # TODO: Тоже самое можно добавить для short_name
        # в экшенах вместо m3.helpers.urls

        self.top_level_packs = []

        # Блокировка для перестроения паттернов урлов
        self._rebuild_lock = threading.RLock()

        # Признак того, что контроллер зарегистрирован во внутреннем кеше
        self._registered = False

    def __str__(self):
        return (
            self.name if self.name
            else super(ActionController, self).__str__()
        )

    #==========================================================================
    # Методы для быстрого поиска экшенов и паков по разным атрибутам
    #==========================================================================
    def _add_action_to_search_dicts(self, action, full_path):
        """ Добавляет экшен в словари для быстрого доступа """
        assert isinstance(action, Action)
        self._actions_by_name[action.get_short_name()] = (action, full_path)
        self._actions_by_type[action.__class__] = (action, full_path)
        self._nodes_by_perm[action.get_perm_code()] = action

    def _add_pack_to_search_dicts(self, pack):
        """ Добавляет экшенпак в словари для быстрого доступа """
        assert isinstance(pack, ActionPack)
        self._packs_by_name[pack.get_short_name()] = pack
        self._packs_by_type[pack.__class__] = pack
        self._nodes_by_perm[pack.get_perm_code()] = pack

    def _rebuild_search_dicts(self):
        """
        Полностью перестраивает поисковые словари.
        Бывает нужно, если иерархия экшенов непредсказуемо изменяется
        """
        self._actions_by_name.clear()
        self._actions_by_type.clear()
        for full_path, v in self._url_patterns.iteritems():
            _, action = v
            self._add_action_to_search_dicts(action, full_path)
        # Обновлять _packs_by_name и _packs_by_type не нужно!

    #==========================================================================
    # Методы формирующие иерархию экшенов и паков
    #==========================================================================
    def _load_class(self, full_path):
        '''
        По полному пути загружает и созвращает класс
        '''
        # Получаем имя модуля и класса в нём
        dot = full_path.rindex('.')
        mod_name = full_path[:dot]
        pack_name = full_path[dot + 1:]
        # Пробуем загрузить
        mod = importlib.import_module(mod_name)
        clazz = getattr(mod, pack_name)
        return clazz

    def _build_pack_node(self, clazz, stack):
        # Что-бы нам не передали, нужно создать экземпляр
        if isinstance(clazz, str):
            clazz = self._load_class(clazz)()
        elif inspect.isclass(clazz):
            clazz = clazz()

        # Отладочный атрибут built нужен,
        # чтобы обнаружить повторное перестроение экшенов
        if hasattr(clazz, '_built'):
            raise ReinitException(clazz=clazz)
        clazz._built = True

        # Присваиваем родителя
        if len(stack) > 0:
            clazz.parent = stack[-1]

        if isinstance(clazz, ActionPack):
            # Для быстрого поиска
            self._add_pack_to_search_dicts(clazz)

            stack.append(clazz)
            # Бежим по экшенам
            for action in clazz.actions:
                self._build_pack_node(action, stack)
            # Бежим по пакам
            for pack in clazz.subpacks:
                self._build_pack_node(pack, stack)
                pack.controller = self
            stack.pop()
        else:
            clazz.controller = self
            full_path = self._build_full_path(stack, clazz)

            # Для быстрого поиска
            self._add_action_to_search_dicts(clazz, full_path)

            self._url_patterns[full_path] = (stack[:], clazz)

    def _invoke(self, request, action, stack):
        '''
        Непосредственный вызов экшена с отработкой всех событий

        :param request: запрос
        :type request: django.http.Request
        '''

        # проверим что права на выполнение есть
        allowed = _permission_checker.has_action_permission(request, action)

        if not allowed:
            # Стандартное сообщение об отсутствии прав
            msg = u'У вас нет прав на выполнение этого действия!'

            # Если разработчик указал verbose_name у action-а,
            # то добавляем название действия
            if action.verbose_name:
                msg = ' '.join([msg, u'Действие:', action.verbose_name])

            return OperationResult.by_message(msg)

        # Заполняем контект и проверяем его
        rules = action.context_declaration()
        context = self.build_context(request, rules)
        try:
            context.build(request, rules)
        except CriticalContextBuildingError:
            # критическая ошибка сбора контекста - должна валиться
            raise
        except ContextBuildingError as e:
            # некритичную ошибку - показываем пользователю
            return OperationResult.by_message(unicode(e))
        except RequiredFailed as e:
            # если контекст неправильный, то возвращаем
            # фейльный результат операции
            return OperationResult.by_message(
                u'Не удалось выполнить операцию. '
                u'Не задан обязательный<br>параметр: ' + e.reason
            )

        # В request заносим информацию о паках и экшене, которые будут
        # выполнены в процессе обработки запроса
        request.target_packs = stack
        request.target_action = action

        try:
            # Все ПРЕ обработчики
            for pack in stack:
                result = pack.pre_run(request, context)
                if result is not None:
                    return result
            # Сам экшен
            result = action.pre_run(request, context)
            if result is not None:
                return result
            response = action.run(request, context)
            result = action.post_run(request, context, response)
            if result is not None:
                return result
            # Все ПОСТ обработчики с конца
            for pack in reversed(stack):
                result = pack.post_run(request, context, response)
                if result is not None:
                    return result
        except ApplicationLogicException as exc:
            return OperationResult(
                success=False, message=exc.exception_message)

        # по возможности запихиваем текущий контекст в response
        if isinstance(response, BaseContextedResult):
            response.set_context(context)
        return response

    def process_request(self, request):
        """
        Обработка входящего запроса *request* от клиента.
        Обрабатывается по аналогии с UrlResolver'ом Django

        :param request: запрос
        :type request: django.http.Request

        :return: результат выполнения экшена
        :rtype: наследник m3_core.actions.results.ActionResult

        :raise: http.Http404
        """
        ControllerCache.populate()

        path = request.path
        matched = self._url_patterns.get(path)
        if matched:
            stack, action = matched

            with _STATSD_CLIENT(self, request):
                try:
                    result = self._invoke(request, action, stack)
                except:
                    if settings.DEBUG:
                    # Записывает сообщение в логгер если включен тестовый режим
                        logger.exception(
                            u'ActionController.process_request: '
                            u'перехвачена необработанная ошибка'
                        )
                    raise

            if isinstance(result, ActionResult):
                return result.get_http_response()

            # Если вернули редирект,
            # то нужно повторно обработать запрос, уже с новым экшеном
            elif isinstance(result, ActionRedirectResult):
                new_path = self.get_action_url(result.action)
                if not new_path:
                    raise ActionNotFoundException(result.action)

                request.path = new_path
                result.prepare_request(request)
                return self.process_request(request)

            return result

        raise http.Http404()

    def build_context(self, request, rules):
        '''
        Выполняет построение контекста вызова операции ActionContext
        на основе переданного request

        :param request: запрос
        :type request: django.http.Request

        :param dict rules: построение контекста

        :return: пустой экземпляр контекста
        :rtype: m3_core.actions.context.DeclarativeActionContext()
            или m3_core.actions.context.ActionContext
        '''
        if DeclarativeActionContext.matches(rules):
            return DeclarativeActionContext()
        else:
            return ActionContext()

    #==========================================================================
    # Методы, предназначенные для поиска экшенов и паков в контроллере
    #==========================================================================
    @_must_be_replaced_by('ControllerCache.find_pack')
    def find_pack(self, type):
        return self._find_pack(type)

    @_must_be_replaced_by('ControllerCache.find_action')
    def find_action(self, type):
        return self._find_action(type)

    def _find_pack(self, pack):
        if isinstance(pack, basestring):
            inst = self._packs_by_name.get(pack)
        elif inspect.isclass(pack):
            inst = self._packs_by_type.get(pack)
        else:
            raise ValueError('Wrong type of argument %s' % pack)
        return inst

    def _find_action(self, action):
        if isinstance(action, basestring):
            inst, _ = self._actions_by_name.get(action, (None, None))
        elif inspect.isclass(action):
            inst, _ = self._actions_by_type.get(action, (None, None))
        else:
            raise ValueError('Wrong type of argument %s' % action)
        return inst

    def get_action_url(self, type):
        """
        Возвращает полный URL адрес для класс или имени класса экшена *action*
        """
        ControllerCache.populate()

        if isinstance(type, str):
            _, full_path = self._actions_by_name.get(type, (None, None))
        elif issubclass(type, Action):
            _, full_path = self._actions_by_type.get(type, (None, None))
        else:
            raise ValueError('Wrong type of argument %s' % type)

        return full_path

    def _find_node_by_perm(self, perm):
        """
        Возвращает экшн/пак по коду права
        """
        return self._nodes_by_perm.get(perm)

    #==========================================================================
    # Методы, предназначенные для добавления/изменения/удаления
    # пакетов действий в контроллер
    #==========================================================================
    def append_pack(self, pack):
        """
        Добавляет пак в контроллер.

        :param pack: пак, который добавляется в контроллер
        :type pack: m3_core.actions.ActionPack
        """
        if not isinstance(pack, ActionPack):
            raise TypeError(
                u'Pack must be an instance of ActionPack subclass!')

        # пак может быть перегруже
        if pack.short_name in ControllerCache.overrides:
            pack = ControllerCache.overrides[pack.short_name]

        self._build_pack_node(pack, [])
        if pack not in self.top_level_packs:
            self.top_level_packs.append(pack)
            pack.controller = self
        ControllerCache.register_controller(self)
        return pack

    def extend_packs(self, packs):
        '''
        Производит массовое добавление экшенпаков в контроллер.

        :param packs: список паков,
            которые необходимо зарегистрировать в контроллере
        :type packs: список объектов m3_core.actions.ActionPack
        '''
        for pack in packs:
            self.append_pack(pack)

    def _norm_url(self, url):
        '''
        Очищает части адреса от мусора.
        Раньше были планы использовать регулярные выражения в адресах,
        сейчас остался мусор от них.
        '''
        for char in ['^', '&', '$', '/']:
            if url.startswith(char):
                url = url[1:]
            if url.endswith(char):
                url = url[:-1]
        return '/' + url if url else ''

    def _build_full_path(self, packs_list, final_action):
        '''
        Возвращает полный адрес от контроллера через паки до конечного экшена.
        '''
        if not final_action.url:
            raise ActionUrlIsNotDefined(final_action)
        return self._norm_url(self.url) + ''.join([
            self._norm_url(x.url) for x in packs_list]
        ) + self._norm_url(final_action.url)

    def wrap_pack(self, dest_pack, wrap_pack):
        '''
        Вставляет экшенпак wrap_pack внутрь иерархии перед dest_pack.
        Таким образом можно перехватывать запросы и ответы пака dest_pack.


        :param dest_pack: Пак который будем оборачивать
        :type dest_pack: m3_core.actions.ActionPack

        :param wrap_pack: Оборачивающий пак
        :type wrap_pack: m3_core.actions.ActionPack
        '''
        assert (
            issubclass(dest_pack, ActionPack) and
            issubclass(wrap_pack, ActionPack)
        )

        """
        Допустим есть цепочка паков:
        A1 - X - A2 - A3    |   A1 - Y - X - A2 - A3
        B1 - B2 - X         |   B1 - B2 - Y - X
        X - C1 - C2         |   Y - X - C1 - C2
        Для решения нужно:
        1. Найти экземпляры пака X
        2. В цепочку вместо X вставить Y->X c учетом левых и правых участников
        3. Перестроить адреса пробежавшись по цепочке
        """
        wrapper = wrap_pack()
        self._add_pack_to_search_dicts(wrapper)
        new_patterns = {}
        current_packs_slice = None
        # списки соседей
        left_packs = []
        right_packs = []

        for url, value in self._url_patterns.iteritems():
            packs_list, final_action = value

            # Поиск пака и соседей в списке
            left_pack = None
            for pos, pack in enumerate(packs_list):
                if pack.__class__ == dest_pack:
                    if pos > 0:
                        left_pack = packs_list[pos - 1]
                    break
            else:
                # Просто копируем
                new_patterns[url] = value
                continue

            # Мутация соседей
            pack.parent = wrapper
            packs_list.insert(pos, wrapper)
            if left_pack:
                wrapper.parent = left_pack
                left_packs.append(left_pack)
            right_packs.append(pack)

            # Создание нового урла
            full_path = self._build_full_path(packs_list, final_action)
            new_patterns[full_path] = (packs_list[:], final_action)
            current_packs_slice = packs_list[:pos + 1]

        self._url_patterns = new_patterns

        # У враппера могут быть собственные экшены и паки.
        # Их тоже нужно построить. Но врапперы также могут быть наследованы
        # от оригинальных паков, поэтому не нужно перестраивать
        # уже существующие экшены и паки, только заменить у них родителя
        if current_packs_slice:
            for subpack in wrapper.subpacks:
                if subpack not in pack.subpacks:
                    self._build_pack_node(subpack, current_packs_slice)

            for action in wrapper.actions:
                if action not in pack.actions:
                    self._build_pack_node(action, current_packs_slice)

            # добавим в левые паки наш врапер,
            # как подчиненный пак без перестройки узлов
            # это нужно для корректной навигации сверху-вниз,
            # а иначе мы никак не узнаем какие паки ниже
            for pack in left_packs:
                if wrapper not in pack.subpacks:
                    pack.subpacks.append(wrapper)
            for pack in right_packs:
                if pack not in wrapper.subpacks:
                    wrapper.subpacks.append(pack)
        else:
            raise ActionPackNotFoundException(dest_pack)

        self._rebuild_search_dicts()

    def wrap_action(self, dest_pack, dest_action, wrap_pack):
        '''
        Вставляет перед экшеном dest_action,
        входящим в пак dest_pack, промежуточный пак wrap_pack.

        ВНИМАНИЕ! Экшены как правило обращаются к своим пакам
        через атрибут "parent", поэтому, вероятно, будут возникать ошибки,
        из-за того, что оборачивающий пак
        не предоставляет методы изначального пака.
        Оборачивающий пак можно наследовать от оригинального,
        но тогда вместо оборачивая целесообразно использовать подмену паков.

        :param dest_pack: Пак в который входит оборачиваемый экшен
        :type dest_pack: m3_core.actions.ActionPack

        :param dest_action: Оборачиваемый экшен
        :type dest_action: m3_core.actions.Action

        :param wrap_pack: Оборачивающий пак
        :type wrap_pack: m3_core.actions.ActionPack
        '''
        assert (
            issubclass(dest_pack, ActionPack) and
            issubclass(wrap_pack, ActionPack) and
            issubclass(dest_action, Action)
        )
        wrapper = wrap_pack()
        self._add_pack_to_search_dicts(wrapper)
        new_patterns = {}
        current_packs_slice = None

        for url, value in self._url_patterns.iteritems():
            packs_list, final_action = value

            # Поиск исходного пака и экшена в нём
            last_pack = packs_list[-1]
            if last_pack.__class__ == dest_pack and (
                    final_action.__class__ == dest_action):
                # Вставка
                packs_list.append(wrapper)
                wrapper.parent = last_pack
                final_action.parent = wrapper

                # Создание нового урла
                full_path = self._build_full_path(packs_list, final_action)
                new_patterns[full_path] = (packs_list[:], final_action)
                current_packs_slice = packs_list

            else:
                # Просто копируем
                new_patterns[url] = value

        self._url_patterns = new_patterns

        if current_packs_slice:
            # У враппера могут быть собственные экшены и паки.
            # Их тоже нужно построить.
            for subpack in wrapper.subpacks:
                self._build_pack_node(subpack, current_packs_slice)
            for action in wrapper.actions:
                self._build_pack_node(action, current_packs_slice)
        else:
            raise ActionPackNotFoundException(dest_pack)

        self._rebuild_search_dicts()

    def dump_urls(self):
        '''
        Отладочный метод.
        Выводит в консоль список всех адрес зарегистрированных в контроллере.
        '''
        print '==== CONTROLLER WITH URL: %s ======' % self.url
        for key in sorted(self._url_patterns.keys()):
            print key
        print
        print 'Total patterns %s' % len(self._url_patterns.keys())

    def get_action_by_url(self, url):
        """
        Получить Action по url
        """
        ControllerCache.populate()

        matched = self._url_patterns.get(url)
        if matched:
            _, action = matched
            return action

    def get_top_actions(self):
        '''
        Получение списка действий или наборов, находящихся на первом уровне
        '''
        top_packs = []
        for stack, act in self._url_patterns.values():
            if len(stack) == 0:
                # значит экшин на верхнем уровне
                if act not in top_packs:
                    top_packs.append(act)
            else:
                if stack[0] not in top_packs:
                    top_packs.append(stack[0])
        return top_packs

    def get_packs(self):
        '''
        Возвращение всех паков в контроллере
        '''
        return self._packs_by_name.values()

    def reset(self):
        '''
        Сброс всего, что наделал контроллер с паками и экшенами
        '''
        for pack in self.top_level_packs:
            if hasattr(pack, '_built'):
                del(pack._built)
                for action in pack.actions:
                    if hasattr(action, '_built'):
                        del(action._built)
        self._actions_by_name.clear()
        self._actions_by_type.clear()
        self._packs_by_name.clear()
        self._packs_by_type.clear()
        self.top_level_packs = []

    @property
    def urlpattern(self):
        """
        Возвращает кортеж вида (pattern, method), пригодный для регистрации
        в urlpatterns Django
        """
        url = self.url
        if url.startswith('/'):
            url = url[1:]
        if url.endswith('/'):
            url = url[:-2]
        return (r'^%s/' % url, self.process_request)


class ControllerCache(object):
    '''
    Внутренний класс платформы,
    который отвечает за хранение кеша контроллеров
    и связанных с ним экшенов и паков.
    '''
    _loaded = False
    _write_lock = threading.RLock()

    # словарь зарегистрированных контроллеров в прикладном приложении
    _controllers = set()

    overrides = {}

    #==========================================================================
    # Методы, предназначенные для поиска экшенов
    # и паков во всех контроллерах системы
    #==========================================================================

    @classmethod
    def get_action_url(cls, type):
        """ Возвращает URL экшена *type* по его имени или классу """
        assert isinstance(type, basestring) or issubclass(type, Action)
        cls.populate()
        for cont in cls._controllers:
            url = cont.get_action_url(type)
            if url:
                return url

    @classmethod
    def find_pack(cls, pack):
        """
        Ищет заданный пак по имени класса или классу
        во всех зарегистрированных контроллерах.
        Возвращает экземпляр первого найденного пака.

        :param pack: имя класса или класс пака
        :type pack: строка формата package.Class или класс

        :return: экземпляр найденного пака
        :rtype: m3_core.actions.ActionPack
        """
        if inspect.isclass(pack):
            name = pack.get_short_name()
        elif isinstance(pack, str) and '.' in pack:
            name = pack
        else:
            raise ValueError(
                u'Pack must be a class or a string '
                u'like "package.subpackage.Class"'
            )

        cls.populate()
        over = cls.overrides.get(name)
        if over:
            return over
        else:
            for cont in list(cls._controllers):
                p = cont._find_pack(pack)
                if p:
                    return p

    @classmethod
    def find_action(cls, action):
        """
        Ищет заданный экшен по имени класса или классу
        во всех зарегистрированных контроллерах.
        Возвращает экземпляр первого найденного экшена.

        :param action: имя класса или класс экшена
        :type action: строка формата package.Class или класс

        :return: экземпляр найденного пака
        :rtype: m3_core.actions.Action
        """
        for cont in list(cls._controllers):
            p = cont._find_action(action)
            if p:
                return p

    @classmethod
    def get_action_by_url(cls, url):
        """ Возвращает Action по переданному *url* """
        for cont in cls._controllers:
            act = cont.get_action_by_url(url)
            if act:
                return act

    @classmethod
    def find_node_by_perm(cls, perm):
        """
        Возвращает экшн/пак по коду права, или None,
        если таковых не найдено
        :param string perm: код права
        :return: экземпляр найденного пара/экшна
        """
        for cont in cls._controllers:
            node = cont._find_node_by_perm(perm)
            if node:
                return node

    #==========================================================================
    @classmethod
    def _self_test(cls):
        """
        Проверяет корректность регистрации экшнов/паков
        """

        cls.populate()

        result = []

        def warn_if_class(obj, name):
            if inspect.isclass(obj):
                result.append(
                    '%r is not the instance but class!' % _name_of(obj))
                return True
            return False

        def shortname_checker(acc):
            def check(obj, name):
                sn = getattr(obj, 'shortname', None) or getattr(
                    obj, 'shortname', None)
                if sn:
                    name = acc.get(sn)
                    if name:
                        result.append(
                            '%r have shortname, reserved for %r!' % (
                                _name_of(obj), name))
                    else:
                        acc[sn] = _name_of(obj)
            return check

        def existense_checker(acc):
            def check(obj, name):
                if name in acc:
                    result.append(
                        '%r already registered!' % name)
                    return True
                acc.add(name)
                return False
            return check

        check_action_sn = shortname_checker({})
        check_pack_sn = shortname_checker({})
        check_reregister = existense_checker(set())

        def check_packs(packs):
            for pack in packs:
                pack_name = _name_of(pack)
                if not warn_if_class(pack, pack_name):
                    check_pack_sn(pack, pack_name)
                    if pack.subpacks:
                        result.append(
                            '%r have subpacks - code smell!' % pack_name)
                        check_packs(pack.subpacks)

                for action in pack.actions:
                    action_name = _name_of(action)
                    if not warn_if_class(action, action_name):
                        if not check_reregister(action, action_name):
                            if check_action_sn(action, action_name):
                                continue

        for cont in cls._controllers:
            check_packs(cont.top_level_packs)

        return result

    @classmethod
    def register_controller(cls, controller):
        '''
        Выполняет регистрацию контроллера *controller* во внутреннем кеше.
        '''
        assert isinstance(controller, ActionController)
        cls._controllers.add(controller)

    @classmethod
    def populate(cls):
        """
        Загружает в кэш ActionController'ы
        из перечисленных в INSTALLED_APPS приложений.
        В каждом из них загружает модуль *app_meta*
        и пытается выполнить метод *register_actions* внутри него.
        Выполняется только один раз. Возвращает истину в случае успеха.

        :return: флаг успешного завершения
        :rtype: boolean
        """
        if cls._loaded:
            return False
        cls._write_lock.acquire()
        try:
            if cls._loaded:
                return False

            cls.overrides = {}
            procs = []
            for app_name in settings.INSTALLED_APPS:
                try:
                    module = importlib.import_module('.app_meta', app_name)
                except ImportError, err:
                    if err.args[0].find('No module named') == -1:
                        raise
                    continue

                # расширение словаря overridde'ов
                for original_cls, new_inst in (
                    getattr(module, 'action_pack_overrides', {}).iteritems()
                ):
                    if inspect.isclass(original_cls):
                        original_cls = original_cls.get_short_name()
                    cls.overrides[original_cls] = new_inst

                # получение ф-ции - регистратора паков
                proc = getattr(module, 'register_actions', None)
                if callable(proc):
                    procs.append(proc)

            # собственно, регистрируем экшны
            for proc in procs:
                proc()

            cls._loaded = True
        finally:
            cls._write_lock.release()
        return True

    @classmethod
    def dump_urls(cls):
        '''
        Отладочный метод.
        Выводит в консоль адреса всех контроллеров зарегистрированных в кэше.
        '''
        print '------------ CONTROLLER CACHE DUMP ------------'
        for cont in cls._controllers:
            cont.dump_urls()

    @classmethod
    def require_update(cls):
        """
        Сбрасывает внутренний флаг заполненности контроллера.
        Следующий запрос к контроллеру вызовет
        перестройку иерархии экшенов и паков.
        """
        for cont in cls.get_controllers():
            cont.reset()
        cls._loaded = False

    @classmethod
    def get_controllers(cls):
        """
        Возвращает множество всех контроллеров зарегистрированных в кэше
        """
        return cls._controllers
