import logging
import datetime
from itertools import takewhile
from random import choice, sample

from django.conf import settings
from yt.wrapper import JsonFormat, TablePath

from intranet.search.core import celery, storages
from intranet.search.core.models import Indexation
from intranet.search.core.errors import RecoverableError, UnrecoverableError
from intranet.search.core.sources.utils import get_resource_data, get_yt_cache_table
from intranet.search.core.storages import FerrymanTableStorage
from intranet.search.core.utils import lock, ydeploy
from intranet.search.core.yt_client import client as yt, create_table_and_insert_rows, delete_rows
from intranet.search.settings.celery.config import ISEARCH_LOCAL_QUEUE_TMPLT
from .document import Document
from .storage import (
    DocumentSerializer,
    SaasDocumentStorage,
    YTDocumentStorage,
    LogbrokerDocumentStorage
)

log = logging.getLogger(__name__)


STORAGES = {s.storage_type: s
            for s in (SaasDocumentStorage, YTDocumentStorage, LogbrokerDocumentStorage)}


class Indexer:
    """Базовый класс для swarm индексаторов
    """
    PRIORITY_LOW = 9
    PRIORITY_MIDDLE = 6
    PRIORITY_HIGH = 3

    # Нужно увеличивать это число на 1 каждый раз,
    # когда индексатор меняется несовместимо со
    # старым потенциально сериализованным объектом
    generation = 1

    # Индексатор поддерживает переход на следующую страницу выдачи
    # api, если сломана предыдущая; другими словами do_walk принимает
    # параметр proceed
    proceed_supported = False

    default_options = {
        'dry_run': False,
        'limit': None,
        'stubdir': None,
        'threads': 4,
        'ts': None,
        'debug': False,
        'noqueue': False,
        'keys': [],
        'shadow_revision': True,
        'new_revision': False,
        'priority': PRIORITY_MIDDLE,  # Приоритет задач. От 0 до 9. Чем меньше тем выше приоритет.
        'nort': True,
        'flush': True,
        'global': False,
        'indexation_id': None,
        'push_id': None,
        'retries': 3,  # сколько раз пытаться выполнить таску
        'load_percent': 50,  # Процент воркеров на которые поступают задания
        'stale_ticks': 30,  # сколько тиков должны не обновлться статусы,
                            # чтобы check решил что индексация испортилась
        'errors_ticks': 20,  # за сколько тиков статиста считать ошибки
        'max_errors_rate': 0.02,  # допустимая доля ошибок за 'errors_ticks'
        'host': None,  # хост на котором запустить индексацию
                       # количество секунд которые статистика по индексации может не
                       # обновляться, по достижении лимита индексация прерывается
        'outdated_statistics_timeout': 3600,
        'no_check': False,
        'suffix': '',
        'document_storages': settings.ISEARCH_DOCUMENT_STORAGES,
        'from_cache': False,  # индексировать из кеша
        'cache_table': False,  # таблица с данными для индексации
    }

    # мапинг стадий на приложения celery. дефолт - local
    pipeline = {
        'setup': 'global',
        'walk': 'global',
        'load': 'global',
        'push': 'global',
        'cleanup': 'global',
        'cleanup_statuses': 'global',
        'check': 'global',

        'fetch': 'local',
        'create': 'local',
        'store': 'local',
    }

    # коэффициенты важности фейлов таски той или иной стадии
    # участвуют в формуле рассчета фейлрейта
    # грубо говоря три фейла контента примерно равны одному фейлу стора
    fail_weights = {
        'setup': 1000,
        'walk': 100,
        'fetch': 3,
        'store': 3,

        'other': 3,
    }

    # при каком проценте выполненных тасок считаем, что стадия завершилась
    done_weights = {
        'setup': 100,
        'walk': 95,
        'fetch': 95,
        'store': 95,
        'other': 80,
    }

    # флаг, указывающий на то, что метод next был вызыван при индексации
    next_called = None

    # атрибуты подлежащие сериализации
    __attrs__ = []

    def __init__(self, options):
        super().__init__()
        self.options = self.default_options.copy()
        self.options.update(options)
        self.options['max_errors_rate'] = float(self.options['max_errors_rate'])
        self.revision = options['revision']
        self.indexation_id = options.get('indexation_id')
        self.push_id = options.get('push_id')

        if 'document_storages' not in options:
            self.options['document_storages'] = (self.search_settings.get('default_storages', [])
                                                 or self.default_options['document_storages'])
        self.document_storages = self.init_document_storages()

        self.group_attr_storage = storages.GroupAttrStorage(self.revision)
        self.facet_storage = storages.FacetStorage(self.revision)
        self.stage_storage = storages.LocalStageStatusStorage(self.revision['id'], self.indexation_id)

        self.indexation_storage = storages.IndexationStorage()
        self.push_storage = storages.PushStorage()

        # Если мы не в рамках индексации, то ничего не кешируем
        self.cache_storage = (storages.CacheStorage(self.indexation_id, self.revision)
                              if self.indexation_id
                              else storages.DummyCacheStorage(self.revision))

        self.revision_storage = storages.RevisionStorage()
        self.profile_storage = storages.ProfileStorage()

        self.yt_table_schema = [
            {'name': 'url', 'type': 'string', 'sort_order': 'ascending'},
            {'name': 'timestamp', 'type': 'int64'},
            {'name': 'raw', 'type': 'any'},
        ]

    def makeindex(self):
        self.create_load_conf()
        self.setup_document_storages()

        if self.push_id:
            self.push_storage.set_instance_status(self.push_id, self.push_storage.STATUS_NEW,
                                                  indexation_id=self.indexation_id)

        self.next('setup')

        if self.options['noqueue']:
            from intranet.search.core.tasks import collect_indexation_stats
            collect_indexation_stats(self.revision['id'], self.indexation_id, True)

            # для индексаций без очереди запускаем чек сразу после выполнения индексации
            self.next('check', _trace=False)

    @property
    def search_settings(self):
        search, index = self.revision['search'], self.revision['index']
        try:
            return settings.ISEARCH['searches']['base'][search]['indexes'][index]
        except KeyError:
            log.exception('Cannot get search settings for %s.%s', search, index)
            return {}

    def init_document_storages(self):
        document_storages = []
        for storage_type in self.options['document_storages']:
            try:
                storage_class = STORAGES[storage_type]
            except KeyError:
                log.error('Unknown storage type: %s', storage_type)
                continue
            document_storages.append(storage_class(self.revision, self.indexation_id))
        if not document_storages:
            log.warning('Indexation does not have any storages')
        return document_storages

    def setup_document_storages(self):
        success = []
        for s in self.document_storages:
            try:
                s.setup()
            except Exception:
                log.exception('Cannot setup storage %s', s)
            else:
                success.append(s)

        if not success:
            self.finish('fail', msg='Cannot setup any storages')
            raise Exception('Cannot setup any storages')
        elif len(success) < len(self.document_storages):
            log.warning('Cannot setup all storages. Setup only: %s', success)
            # Выкидываем стораджи, которые не смогли установить, из настроек индексатора
            self.document_storages = success
            self.options['document_storages'] = [s.storage_type for s in success]
        else:
            log.info('Setup all storages successfully: %s', self.document_storages)

    @property
    def stage_stats(self):
        if not hasattr(self, '_stage_stats'):
            self._stage_stats = self.indexation_storage.get_stage_stats(
                self.indexation_id, self.revision['id'],
            )
            log.info('Got stage stats data: %s, ticks: %s',
                     self._stage_stats.data, self._stage_stats.tick)
        # кешировать не страшно, потому что атрибут _stage_stats
        # потеряется после сериализации/десериализации
        return self._stage_stats

    @property
    def is_delta(self):
        """ Говорит, является ли индексация дельтой (частичной индексацией) или нет
        """
        # дельтой считается индексация по времени или индексация по ключам,
        # если ничего из этого не указано - то переиндексация полная
        return bool(self.options['ts'] or self.options['keys'] or not self.indexation_id)

    def get_initial_load_conf(self):
        """ Задает количество партиций для индексации
        """
        load_conf = {}
        norm_percent = self.options['load_percent']

        for queue, partitions in ydeploy.get_queues():
            max_parts = int(partitions)

            if max_parts > 1:
                cfg_parts = int(norm_percent / 100.0 * max_parts) or 1
                load_conf[queue] = sorted(sample(range(max_parts), cfg_parts))

        return load_conf

    def create_load_conf(self):
        load_conf = self.get_initial_load_conf()
        log.debug('Inital load conf: %s', load_conf)
        self.indexation_storage.update(self.indexation_id, load_conf=load_conf)

    def _need_trace(self, trace):
        """ Говорит о том, нужно ли отслеживать статусы тасок в статистике
        """
        if not self.indexation_id:
            return False
        return trace

    def next(self, stage, _countdown=None, _trace=True, _status_id=None,
             _queue=None, _expires=None, **kwargs):
        from intranet.search.core.tasks import swarm_stage
        if self.indexation_id is not None:
            load_conf = self.indexation_storage.get_cached(self.indexation_id)['load_conf']

        # проверка на пустой load_conf нужна для индексаций которые
        # были до момента введения ограничений нагрузки
        if self.indexation_id is None or not load_conf:
            load_conf = self.get_initial_load_conf()

        if not self.options.get('global', False):
            app = self.pipeline.get(stage, 'local')
        else:
            app = 'global'

        if _status_id is None and self._need_trace(_trace):
            _status_id = self.stage_storage.create(stage=stage, app=app)

        kwargs['_status_id'] = _status_id
        log.info('Stage `%s`.`%s` queued', stage, _status_id)

        queue = _queue or stage
        if queue in load_conf:
            partition = str(choice(load_conf[queue]))
        else:
            partition = ''

        suffix = self.options.get('suffix', '') if stage == 'walk' else ''

        queue_name = queue + (suffix or str(partition))

        log.debug('Queue for %s.%s: %s', stage, _status_id, queue_name)

        with celery.switch_app(app, swarm_stage):
            stage_method = swarm_stage.apply if self.options['noqueue'] else swarm_stage.apply_async
            stage_method(
                args=(self, stage),
                kwargs=kwargs,
                queue=queue_name,
                countdown=_countdown,
                expires=_expires,
                priority=self.options['priority'],
            )

        self.next_called = True

    def get_yt_cache_table(self):
        path = get_yt_cache_table(self.revision['search'], self.revision['index'])
        return TablePath(path, append=True)

    def do_content(self, url, raw_data=None, updated_ts=None, delete=False, **kwargs):
        if self.options['dry_run']:
            log.debug('Dry run. Do not content document: %s', url)
            return

        try:
            if delete:
                delete_rows(self.get_yt_cache_table(), [{'url': url}])
            else:
                row = {
                    'url': url,
                    'raw': raw_data,
                    'timestamp': updated_ts,
                }
                create_table_and_insert_rows(self.get_yt_cache_table(), [row], self.yt_table_schema)
        except Exception as e:
            log.exception('Error in stage content %s', e)

    def do_store(self, document, body_format='json', delete=False, **kwargs):
        if self.options['dry_run']:
            log.debug('Dry run. Do not store document: %s', document)
            return

        if delete:
            for st in self.document_storages:
                st.delete(document)
            return

        kwargs = {'realtime': not(self.options['nort'])}
        serializer = DocumentSerializer(document, self.revision, body_format)
        serialized_document = serializer.format_document_for_update(**kwargs)
        for st in self.document_storages:
            st.update(document, serialized_document, **kwargs)

        self.group_attr_storage.create(*document.attributes['group'])
        self.facet_storage.create(*document.attributes['facet'])

    def do_stage(self, stage_name, _status_id=None, _retries=None, **kwargs):
        log.info('Stage `%s`.`%s` started', stage_name, _status_id)

        stage = getattr(self, 'do_%s' % stage_name)
        self.next_called = False

        if _retries is None:
            _retries = self.options.get('retries', 1)

        if self.indexation_id:
            if not self.indexation_storage.is_stage_allowed(self.indexation_id,
                                                            stage_name):
                self.stage_storage.cancel(_status_id)
                log.info('Stage `%s`.`%s` canceled', stage_name, _status_id)
                return

        self.stage_storage.start(_status_id)

        try:
            stage(**kwargs)
        except UnrecoverableError:
            # Если нет надежды что ошибка пройдет, то сразу прекращяем стадию
            status = self.stage_storage.fail(_status_id)
            log.exception('Stage `%s`.`%s` failed with unrecoverable error:', stage_name, _status_id)
            raise
        except RecoverableError:
            # Если возможно ошибка пройдет, то пробуем сделать retry
            log.exception('Stage `%s`.`%s` failed with recoverable error:', stage_name, _status_id)
            _retries -= 1

            if _retries < 0:
                status = self.stage_storage.fail(_status_id)
                log.exception('Stage `%s`.`%s`. Retry limit exceeded', stage_name, _status_id)
                raise
            else:
                status = self.stage_storage.retry(_status_id)
                log.warning('Stage `%s`.`%s`. Try to retry. Retries left: %s',
                            stage_name, _status_id, _retries)
                kwargs['_countdown'] = 2 ** (self.options['retries'] - _retries)

                if stage_name == 'walk' and self.proceed_supported:
                    # если walk зафейлил взять свою страницу, то перед raise ошибки
                    # он вызывает walk для следующей страницы, а сам переходит в статус retry
                    # если не запретить ему обход каталога, то индексация раздвоится
                    # TODO: этим надо управлять как-то иначе
                    kwargs['proceed'] = False

                self.next(stage_name, _status_id=_status_id, _retries=_retries, **kwargs)
        except:
            # Если какая-то непонятная ошибка, то тоже сразу фейлим стадию
            status = self.stage_storage.fail(_status_id)
            log.exception('Stage `%s`.`%s` failed with unknown error:', stage_name, _status_id)
            raise
        else:
            status = self.stage_storage.succeed(_status_id)
            log.info('Stage `%s`.`%s` succeed', stage_name, _status_id)
        finally:
            self.update_push_instance(stage_name, status)

    def update_push_instance(self, stage, stage_status):
        log.debug('Update push instance started: stage=%s, stage_status=%s, push_id=%s, '
                  'indexation_id=%s, next_called=%s',
                  stage, stage_status, self.push_id, self.indexation_id, self.next_called)
        if self.indexation_id or not self.push_id:
            return

        if stage_status in ('fail', 'cancel') or (
                stage_status == 'done' and (stage == 'store' or not self.next_called)):
            # Обновляем статус единичного пуша при любом фейле/отмене и при успешном
            # завершении стора или при успешном завершении стадии, после которой
            # ничего уже не будет запущено
            push_status = self.push_storage.get_push_status_by_stage(stage_status)
            self.push_storage.set_instance_status(self.push_id, status=push_status,
                                                  revision_id=self.revision['id'])

    def do_check(self, **kwargs):
        indexation_info = self.indexation_storage.get(self.indexation_id)
        indexation_status = indexation_info['status']
        log.debug('Indexation info: %s', indexation_info)

        if indexation_status in ('fail', 'done'):
            log.debug('Finish check. Reason: indexation_status == %s', indexation_status)
            return

        self.indexation_storage.touch(self.indexation_id)
        if self.is_statistics_outdated(indexation_info):
            log.debug('Statistics is outdated')
            self.finish('fail',
                        msg='Indexation statistics is outdated. '
                        'Check statisticians and celerybeat.')
            return

        if not self.is_check_relevant():
            log.debug('Check is not relevant')
            return

        if self.is_fails_rate_exceeded():
            log.debug('Fails rate exceeded')
            self.finish('fail', msg='Fails rate exceeded')
            return

        if self.is_indexation_finished(indexation_info):
            log.debug('Indexation is finished')
            self.finish('done', msg='OK')
            return

        if self.is_indexation_stale():
            if self.is_indexation_satisfactory():
                log.debug('Indexation is satisfactory')
                self.finish('done', msg='Indexation is satisfactory')
            else:
                log.debug('Indexation is stale')
                self.finish('fail', msg='Stale indexation detected')
            return

        # останавливаем волки если уже достаточно много поставили качать
        if indexation_info['walk'] and self.is_limit_reached():
            log.debug('Stopping walks %s', self.indexation_id)
            self.stop_stage('walk', msg='Walk limit reached')

        # если количество фетчей в done уже достаточное то останавливаемся
        if indexation_status != 'stop' and self.is_limit_reached(done=True):
            log.debug('Stopping fetches %s ', self.indexation_id)
            self.stop(msg='Fetch limit (%s) reached' % self.options['limit'])

        # подновляем количество документов
        count = self.stage_stats.count(stage='store', status='done')
        self.indexation_storage.update(self.indexation_id, documents_count=count)

    def do_setup(self, **kwargs):
        """ Настраивает индексацию. По умолчанию запускает первый walk или load
        в зависимости от настройки кеширования.
        """
        stage = 'load' if self.options['from_cache'] else 'walk'
        self.next(stage)

    def do_walk(self, **kwargs):
        raise NotImplementedError

    def do_load(self, **kwargs):
        """ Обход данных источника в кеше
        """
        cache_table = self.options['cache_table']
        if not cache_table:
            cache_table = FerrymanTableStorage.get_yt_table_path(
                search=self.revision['search'],
                index=self.revision['index']
            )

        # Используем json формат, потому что хотим получить данные в unicode,
        # а не байтовые строки (yson вернет именно их).
        # При этом явно указываем encode_utf8=False, потому что в наших таблицах
        # уже хранятся правильно закодированные строки. С невыключенным флагом
        # encode_utf8 yt.wrapper творит дичь, и на выходе мы получаем кривую
        # юникодную строку: u'\xd0\xba\xd0\xb0\xd0\xba\xd0\xb8\xd0\xb5-\xd1\x82\xd0\xbe',
        # которую уже никак нормально не раскодировать.
        # Документация: https://wiki.yandex-team.ru/yt/userdoc/formats/#json
        row_format = JsonFormat(attributes={'encode_utf8': False})
        for row in yt.read_table(cache_table, format=row_format):
            if row['deleted'] or not row['raw']:
                continue

            data = self._parse_cache_row(row)
            self.next('create', **data)

    def _parse_cache_row(self, row):
        """ Парсит yql строку и возвращает словарь параметров, которые будут переданы в do_create
        """
        return row['raw']

    def do_push(self, **kwargs):
        raise NotImplementedError

    def do_cleanup(self, purge=False, **kwargs):
        """ Завершающие действия по окончанию индексации (успешному или нет)
        """
        # перед удалением всего из базы соберем последний раз статистику
        from intranet.search.core.tasks import collect_stats
        log.debug('Run sync statistics collection for indexation %s', self.indexation_id)
        collect_stats(self.indexation_id, sync=True)

        if self.options.get('flush', True):
            for st in self.document_storages:
                st.flush(delta=self.is_delta, realtime=(not self.options['nort']))
        else:
            log.info('Option flush is False. Skip flushing.')

        self.cache_storage.clear()

        if purge and not self.options['dry_run']:
            for st in self.document_storages:
                st.purge()
            self.group_attr_storage.purge()
            self.facet_storage.purge()

            self.revision_storage.purge(self.revision['id'])
            self.indexation_storage.purge(self.indexation_id)

        self.release_lock()

        # зачищаем статусы в редисах
        for host in self.indexation_storage.get_hosts(self.indexation_id, self.revision['id']):
            self.next('cleanup_statuses',
                      _queue=ISEARCH_LOCAL_QUEUE_TMPLT % host, _trace=False)
        self.indexation_storage.archive_stage_stats(self.indexation_id, self.revision['id'])

    def release_lock(self):
        """ Снимаем блокировку индексации
        """
        lock_context = self.options.get('lock_context')

        if lock_context:
            log.debug('Releasing lock...')

            indexation_lock = lock.lock_from_context(lock_context)
            try:
                if indexation_lock.release():
                    log.debug('Lock is released successfully')
                else:
                    log.debug('Lock cannot be released')
            except Exception as e:
                log.warning('Got an error while releasing lock: %s', e)

    def do_cleanup_statuses(self, **kwargs):
        """ Удаляет все статусы индексации """
        self.stage_storage.delete()

    def index_resource(self, resource, **kwargs):
        data = get_resource_data(resource)
        self.next('push', data=data, **kwargs)

    def delete_resource(self, resource):
        data = get_resource_data(resource)
        self.next('push', data=data, delete=True)

    def stop_stage(self, stage, msg=None):
        assert stage in self.stage_storage.stages, "Wrong stage name"
        log.info('Stage %s disabled: %s', stage, msg)
        kws = {stage: False}
        self.indexation_storage.update(self.indexation_id, msg=msg, **kws)

    def stop(self, msg=None):
        """Мягко останавливаем индексацию
        """
        log.info('Indexation stopped with: %s', msg)
        self.indexation_storage.update(self.indexation_id, walk=False, fetch=False,
                                       status='stop', msg=msg)

    def finish(self, status='done', msg=None):
        """Жестко завершаем индексацию
        """
        log.info('Indexation finished with status: %s; %s', status, msg)

        data = {
            'status': status,
            'documents_count': self.stage_stats.count(stage='store', status='done'),
            'end_time': datetime.datetime.now(),
        }
        data.update({stage: False for stage in self.stage_storage.stages if stage != 'push'})

        self.indexation_storage.update(self.indexation_id, msg=msg, **data)

        if status == 'done':
            revision = self.revision_storage.get(self.revision['id'])
            revision_status = revision['status']
            if revision_status == 'deleted':
                pass
            elif revision_status != 'active' and self.options['shadow_revision']:
                self.revision_storage.set_ready(self.revision['id'])
            else:
                self.revision_storage.set_active(self.revision)
        elif status != 'paused':
            # при фейле индексации не флашим ничего в стораджи
            self.options['flush'] = False
            if self.revision['status'] == 'new':
                self.revision_storage.set_broken(self.revision['id'])
        else:
            self.options['flush'] = True

        if self.push_id:
            push_status = self.push_storage.get_push_status_by_indexation(status)
            self.push_storage.set_instance_status(self.push_id, status=push_status,
                                                  indexation_id=self.indexation_id)
        self.next('cleanup', _trace=False)

    def pause(self, msg=None):
        self.finish(status='paused', msg=msg)

    def continue_indexation(self, **options):
        data = {'status': Indexation.STATUS_NEW}
        data.update({stage: True for stage in self.stage_storage.stages if stage != 'push'})
        self.indexation_storage.update(self.indexation_id, msg='enabling new stages for this indexation', **data)
        index = self.indexation_storage.get(self.indexation_id)
        for key in options:
            self.options[key] = options[key]
        logging.info('last_wiki_pk is %s', index.get('last_wiki_pk'))
        if index.get('last_wiki_pk'):
            self.options['last_wiki_pk'] = int(index['last_wiki_pk'])
        self.makeindex()

    # Вспомогательные методы

    def is_limit_reached(self, done=False):
        """Проверяет достигнут ли заданный лимит индексации
        если done==False, то считаем общее количество фетчей в любом статусе
        если done==True, то считаем количество фетчей в статусе done
        """
        limit = self.options['limit']
        if limit:
            if done:
                fetches = self.stage_stats.count(stage='fetch', status='done')
                log.debug('CHEKER, fetches in status done: %s', fetches)
            else:
                fetches = self.stage_stats.count(stage='fetch')
                log.debug('CHEKER, total fetches: %s', fetches)

            if fetches >= limit:
                log.debug('Limit reached, fetches: %s', fetches)
                return True
        else:
            return False

    def is_check_relevant(self):
        """Говорит, нужно ли чтобы check реально начал работать
        """
        # Если при запуске индексации решили, что чек ей не нужен
        return not self.options['no_check']

    def is_indexation_finished(self, info):
        """Проверяет, что все стадии индексации кончились.
        Т.е. когда все таски находятся в каком-то конечном состоянии
        """
        finished_statuses = ('done', 'fail', 'cancel')
        not_finished_stages_count = self.stage_stats.count(status=('new', 'in progress', 'retry'))
        finished_stages_count = self.stage_stats.count(status=finished_statuses)
        # Проверяем, что ни какой стадии нет задач в работающем состоянии и что хотя бы одна стадия
        # дошла до конца. Это нужно, чтобы не считать завершившимися индексации, у которых
        # в статистике пусто из-за каких-то проблем
        is_all_finished = not_finished_stages_count == 0 and finished_stages_count > 0
        expected_docs = info.get('expected_documents_count')

        if is_all_finished and expected_docs is not None:
            finished_docs = self.stage_stats.count(stage=['store'], status=finished_statuses)
            failed_docs = self.stage_stats.count(stage=['create', 'fetch'], status=('fail', 'cancel'))
            log.debug('Expected docs: %s, get finished: %s, failed: %s',
                      expected_docs, finished_docs, failed_docs)
            return expected_docs <= finished_docs + failed_docs
        else:
            return is_all_finished

    def is_indexation_satisfactory(self):
        """Проверяет, можно ли считать зависшую индексацию успешной
        (ситуация, когда счетчики меняться перестали, но в ноль не сошлись)
        """
        # считаем, на сколько процентов индексация выполнилась
        done_percents = {}
        for stage in self.stage_stats.data.keys():
            done = self.stage_stats.count(stage=stage, status='done')
            total = self.stage_stats.count(stage=stage)
            if total:
                done_percents[stage] = (float(done) / total) * 100

        for stage, percent in done_percents.items():
            if percent < self.done_weights.get(stage, self.done_weights['other']):
                return False

        return True

    def is_fails_rate_exceeded(self):
        """Проверяет, что не превышен рейт ошибок
        """
        ticks = self.options['errors_ticks']

        assert 'other' in self.fail_weights, "Wrong fail_weights settings"

        fails = {}
        stages = {}

        # считаем количество фейлов по всем стадиям за несколько последних тиков
        # то есть не общее число, а срез во времени, поэтому deltas, а не count
        for stage in self.fail_weights:
            if stage != 'other':
                fcount = sum(
                    self.stage_stats.deltas(status='fail', stage=stage)[-ticks:]
                )
                tcount = sum(
                    self.stage_stats.deltas(stage=stage)[-ticks:]
                )

                fails[stage] = fcount
                stages[stage] = tcount
        failed_count = sum(self.stage_stats.deltas(status='fail')[-ticks:])
        stages_count = sum(self.stage_stats.deltas()[-ticks:])

        fails['other'] = failed_count - sum(fails.values())
        stages['other'] = stages_count - sum(stages.values())

        # домножаем количество на вес стадии
        failed_score = stage_score = 0.0
        for stage, koeff in self.fail_weights.items():
            failed_score += fails[stage] * koeff
            stage_score += stages[stage] * koeff

        if stage_score > 0:
            failes_rate = failed_score / stage_score
            log.debug('Fail rate %s', failes_rate)
            return failes_rate > self.options['max_errors_rate']
        else:
            return False

    def is_indexation_stale(self):
        """Проверяет, что индексация не протухла и идет
        """
        deltas = self.stage_stats.deltas()
        stale_ticks = self.options['stale_ticks']

        if len(deltas) < stale_ticks:
            return False

        # посчитаем количество нулей с конца
        zero_ticks = len(list(takewhile(lambda x: x == 0, reversed(deltas))))
        return zero_ticks > stale_ticks

    def is_statistics_outdated(self, info):
        hosts_info = self.indexation_storage.get_hosts(
            self.indexation_id, self.revision['id'], with_date=True)
        log.debug('Hosts info: %s', hosts_info)

        timeout = datetime.timedelta(seconds=self.options['outdated_statistics_timeout'])
        now = datetime.datetime.now()

        if hosts_info:
            last_update = max(date for host, date in hosts_info)
        else:
            last_update = info['start_time']

        log.info('Hosts info: %s, last_updated: %s', hosts_info, last_update)
        return (now - last_update) > timeout

    def create_document(self, doc_url, updated=None, raw_data=None):
        document = Document(doc_url, updated=updated, raw_data=raw_data)

        # Добавляем в документ то, что в нем всегда должно быть
        # и не зависит от конкретного индексатора
        document.emit_property_attr('doc_url', document.url)
        document.emit_property_attr('doc_id', document.url)
        document.emit_property_attr('doc_source', self.revision['search'])
        document.emit_property_attr('doc_index', self.revision['index'])
        document.emit_property_attr('doc_revision', self.revision['id'])

        document.emit_search_attr('s_doc_source', self.revision['search'])
        document.emit_search_attr('s_doc_index', self.revision['index'])
        document.emit_search_attr('i_indexation_id', int(self.indexation_id or 0))
        document.emit_search_attr('i_revision_id', int(self.revision['id']))
        document.emit_search_attr('i_indexation_ts', int(datetime.datetime.now().strftime('%s')))

        return document

    def __reduce__(self):
        return (
            self.__class__,
            (self.options, ),
            {attr: getattr(self, attr) for attr in self.__attrs__},
        )
