import logging
from datetime import datetime, timedelta
from time import sleep
from uuid import uuid4

from celery import shared_task
from django import db
from django.db import DatabaseError, InterfaceError
from django.db.transaction import get_connection
from ylock.decorators import locked
from ylog.context import log_context

from intranet.search.core.storages import (
    FacetStorage,
    GroupAttrStorage,
    IndexationStorage,
    PushStorage,
    RevisionStorage,
)
from intranet.search.core.swarm.pushes import restart_push
from intranet.search.core.utils import lock


log = logging.getLogger(__name__)


@shared_task(bind=True, name='isearch.tasks.swarm_stage')
def swarm_stage(self, indexer, stage_name, *args, **kwargs):
    stage_context = dict(
        task_id=self.request.id,
        indexation_id=indexer.indexation_id or 0,
        organization_id=indexer.revision['organization']['id'],
        revision_id=indexer.revision['id'],
        search=indexer.revision['search'],
        stage=stage_name,
    )
    if indexer.push_id:
        stage_context['push_id'] = indexer.push_id

    with log_context(**stage_context):
        try:
            return indexer.do_stage(stage_name, *args, **kwargs)
        except db.Error:
            # Ретраим на этом уровне только их.
            # Например, не смогли обновить статусы.
            log.exception('Database failure while processing swarm task:')
            raise self.retry(max_retries=indexer.options['retries'],
                             countdown=2 ** self.request.retries)


@shared_task(name='isearch.tasks.clear_pushes')
@locked('clear_pushes', lock.manager, block=False)
def clear_pushes(days=5):
    """ Удаляет старые записи в логе пушей
    """
    log.info('Start to clear pushes older than %s days', days)
    PushStorage().delete_older_than(datetime.now() - timedelta(days=days))


@shared_task(name='isearch.tasks.update_pushes')
@locked('update_pushes', lock.manager, block=False)
def update_pushes(stale_hours=1):
    """ Обновляет статусы для незавершённых пушей
    """
    push_storage = PushStorage()
    stale_threshold = datetime.now() - timedelta(hours=stale_hours)
    push_storage.fail_staled_instances(stale_threshold)
    push_storage.update_statuses()


@shared_task(name='isearch.tasks.restart_pushes')
@locked('restart_pushes', lock.manager, block=False)
def restart_pushes(max_retries=3):
    """ Перезапуск упавших пушей
    """
    push_storage = PushStorage()
    comment = 'auto retry failed push'

    for push_id in push_storage.get_pushes_for_retry(max_retries):
        restart_push(push_id, only_failed=True, comment=comment)
    # чтобы локи не снимались слишком быстро
    sleep(5)


@shared_task(name='isearch.tasks.run_check')
@locked('run_check', lock.manager, block=False)
def run_check(seconds=60):
    """ Запуск проверки статистики всех работающих индексаций
    :param seconds: количество секунд между чеками
    """
    indexation_storage = IndexationStorage()

    for indexation_id in indexation_storage.get_with_stale_check(seconds):
        with log_context(indexation_id=indexation_id, check_id=uuid4().hex, method='run_check'):
            try:
                indexer = indexation_storage.get_indexer(indexation_id)
                if not indexer:
                    log.warning('Cannot load indexer for indexation: %s', indexation_id)
                    continue
                indexation_storage.touch(indexation_id)
                indexer.next('check', _trace=False)
            except (DatabaseError, InterfaceError) as e:
                log.exception('Indexation check failed: %s', e)
                # нужно переоткрыть соединение к базе, после ошибки в бд
                connection = get_connection()
                connection.close()
                connection.ensure_connection()
            except Exception as e:
                log.exception('Indexation check failed: %s', e)


@shared_task(name='isearch.tasks.flush_buffered_attributes')
def flush_buffered_attributes(revision_id=None):
    """ Собирает из локальных буфферов группировочные атрибуты и фасеты
    и кладет в базу,

    если не передан revision_id, то запускается для всех активных ревизий
    """
    rev_storage = RevisionStorage()

    if revision_id is None:
        buffered_revisions = set(FacetStorage.get_buffered_keys()) | set(GroupAttrStorage.get_buffered_keys())
        revisions = rev_storage.get_by_id_list(list(buffered_revisions))
    else:
        revisions = [rev_storage.get(revision_id)]

    for revision in revisions:
        FacetStorage(revision).flush()
        GroupAttrStorage(revision).flush()


@shared_task(name='isearch.tasks.clear_indexations')
@locked('clear_indexations', lock.manager, block=False)
def clear_indexations(days=7):
    """ Удалаяет все старые завершенные индексации
    """
    log.info('Start to clear indexations older than %s days', days)
    dt = datetime.now() - timedelta(days=days)
    IndexationStorage().delete_older_than(dt)
