import datetime
import logging
from typing import List, Set, Dict, Any
from dateutil.relativedelta import relativedelta
import celery
from staff.lib import waffle

from django.core.urlresolvers import reverse
from django.conf import settings

from staff.celery_app import app
from staff.departments.models import ProposalMetadata
from staff.lib import requests, startrek
from staff.lib.db import atomic
from staff.lib.lock import lock_manager, LOCK_TRANSACTION_TIMEOUT
from staff.lib.log import log_context
from staff.person.models import Staff

from staff.budget_position.workflow_service import workflow_registry_service
from staff.departments.controllers.proposal import (
    ApprovementStatus,
    ApprovementTicketType,
    ProposalCtl,
)
from staff.departments.controllers.proposal_action import PERSON_ACTION_SECTIONS
from staff.departments.controllers.proposal_execution import ProposalExecution
from staff.departments.controllers.tickets import (
    HeadcountTicket,
    ProposalContext,
    ProposalTicketDispatcher,
    PersonTicket,
    RestructurisationTicket,
    RestructurisationLinkedTicket,
    ValueStreamTicket,
)
from staff.proposal.controllers import (
    ProposalTasks,
    approvement as approvement_ctl,
)

logger = logging.getLogger('staff.proposal.tasks')


@app.task
class ExecuteProposal(app.Task):

    def run(self, proposal_id, author_login=None):
        with log_context(task_name='ExecuteProposal', proposal_id=proposal_id, author_login=author_login):
            self.run_execution(proposal_id, author_login=author_login)

    def run_execution(self, proposal_id, author_login=None):
        proposal_ctl = ProposalCtl(proposal_id)
        author_login = author_login or proposal_ctl.author.login
        logger.info('Going to execute proposal %s', proposal_id)
        try:
            with atomic():
                ProposalExecution(proposal_ctl).execute(execution_author_login=author_login)

                if proposal_ctl.splitted_to:
                    ProposalTasks.schedule_ordered_task(
                        ProposalMetadata.objects.get(proposal_id=proposal_ctl.splitted_to).id,
                        callable_or_task=SyncProposalTickets,
                        kwargs={
                            'author_login': author_login,
                            'proposal_id': proposal_ctl.splitted_to,
                            'proposal_diff': {},
                            'updated_logins': [],
                            'deleted_logins': [],
                        }
                    )

                proposal_metadata = ProposalMetadata.objects.get(proposal_id=proposal_id)
                ProposalTasks.schedule_ordered_task(
                    proposal_metadata.id,
                    callable_or_task=on_after_execute,
                    kwargs={'proposal_id': proposal_id, 'author_login': author_login},
                )

                ProposalTasks.schedule_ordered_task(
                    proposal_metadata.id,
                    callable_or_task=notify_review,
                    kwargs={'proposal_id': proposal_id}
                )
        except Exception as e:
            logger.info('Error during %s proposal execution', proposal_id, exc_info=True)
            proposal_ctl.rollback_actions_state(str(e))
            proposal_ctl.unlock()
            try:
                self.notify_ticket_about_err(proposal_ctl, author_login)
            except Exception:
                logger.exception('Error trying to post error to ticket for proposal %s', proposal_id)
            raise

    @staticmethod
    def notify_ticket_about_err(proposal_ctl: ProposalCtl, author_login: str):
        err_text = 'При выполнении заявки произошла ошибка. Структура на стафф не отражена.'
        if proposal_ctl.restructurisation_ticket:
            RestructurisationTicket.from_proposal_ctl(proposal_ctl).add_comment(author_login, err_text)


def _budget_position_registry_url(proposal_id: str):
    registry_url = (
        f'https://{settings.STAFF_HOST}/admin/budget_position/workflow/?q={proposal_id}&status__exact=confirmed'
    )
    return registry_url


@app.task
def on_after_execute(proposal_id, author_login):
    proposal_ctl = ProposalCtl(proposal_id)
    proposal_context = ProposalContext(proposal_ctl)

    notify_person_tickets(proposal_context=proposal_context)

    if proposal_ctl.splitted_to:
        new_proposal_context = ProposalContext.from_proposal_id(proposal_ctl.splitted_to)
        notify_splitted_person_tickets(new_proposal_ctx=new_proposal_context)

    if proposal_ctl.restructurisation_ticket:
        linked = proposal_context.proposal_object['tickets'].get('department_linked_ticket')
        comment = 'Заявка успешно выполнена.'
        if linked:
            proposal_url = reverse('departments-frontend:edit_proposal', kwargs={'proposal_id': proposal_id})
            comment = (
                f'Завершена работа по дополнительной заявке '
                f'((https://{settings.STAFF_HOST}{proposal_url} {proposal_id}))'
            )

        comment += f'\nЭта заявка в реестре {_budget_position_registry_url(proposal_id)}'
        RestructurisationTicket.from_proposal_ctl(proposal_context.proposal).add_comment(
            author_login=author_login,
            comment_text=comment,
        )
    if proposal_ctl.is_auto_applied:
        metadata_id = ProposalMetadata.objects.get(proposal_id=proposal_id).id
        workflow_registry_service.WorkflowRegistryService().push_proposal_to_oebs(metadata_id)


def notify_person_tickets(proposal_context: ProposalContext):
    section_to_text = {
        PERSON_ACTION_SECTIONS.department: 'подразделение',
        PERSON_ACTION_SECTIONS.office: 'офис',
        PERSON_ACTION_SECTIONS.organization: 'организация',
        PERSON_ACTION_SECTIONS.position: 'должность',
    }
    for person_act in proposal_context.proposal.person_action_objs:
        action_texts = [section_to_text[sec] for sec in person_act.sections if sec in section_to_text]
        if not action_texts:
            continue
        text = f'После выполнения заявки были изменены следующие данные на стаффе: {", ".join(sorted(action_texts))}\n'
        text += f'Эта заявка в реестре {_budget_position_registry_url(proposal_context.proposal_id)}'

        PersonTicket(proposal_context, person_act.login).add_comment_if_has_ticket(text)


def notify_splitted_person_tickets(new_proposal_ctx: ProposalContext):
    proposal_id = new_proposal_ctx.proposal_id
    for person_action in new_proposal_ctx.proposal.person_actions:
        proposal_url = f'https://{settings.STAFF_HOST}{reverse("proposal-frontend:edit_proposal", args=[proposal_id])}'
        text = f'Заявка была расщеплена, этот сотрудник выделен в новую заявку: (({proposal_url}  {proposal_id}))'

        PersonTicket(new_proposal_ctx, person_action['login']).add_comment_if_has_ticket(text)


@app.task(bind=True)
def notify_review(self, proposal_id):
    url_to_notify = settings.REVIEW_URL + '/v1/staff-structure-push/'
    token = 'OAuth {token}'.format(token=settings.ROBOT_STAFF_OAUTH_TOKEN)

    try:
        requests.post(
            url_to_notify,
            json={'proposal_id': proposal_id},
            headers={'Authorization': token},
            timeout=(0.3, 1, 3),
        )
    except requests.RequestException:
        logger.exception(
            'Review request %s failed for %s proposal',
            url_to_notify,
            proposal_id,
        )
        raise self.retry()


@app.task
def create_persons_tickets(
    author_login: str = '',
    proposal_context: ProposalContext = None,
    proposal_id: str = '',
    logins: List[str] = None,
) -> None:
    """
    Создаёт персональные тикеты по указанным логинам от имени author_login либо автора заявки
    или по всем, кому `ProposalTicketDispatcher` считает нужным иметь персональный тикет,
    если список `logins` не передать
    """
    adding_to_existing_proposal = logins is not None
    proposal_id = proposal_context.proposal_id if proposal_context else proposal_id
    proposal_context = proposal_context or ProposalContext.from_proposal_id(proposal_id)
    author_login = author_login or proposal_context.proposal.author.login

    with log_context(proposal_id=proposal_id or proposal_context.proposal_id, logins=logins or 'ALL'):
        logins = logins or [act['login'] for act in ProposalTicketDispatcher(proposal_context).personal]

        for login in logins:
            logger.info('Going to create personal ticket for %s (%s)', login, proposal_id)
            try:
                personal_ticket_ctl = PersonTicket(proposal_context, login)
                key = personal_ticket_ctl.create_ticket(
                    author_login=author_login,
                    adding_to_existing_proposal=adding_to_existing_proposal,
                )
                update_approvement(
                    proposal_context,
                    key,
                    ApprovementTicketType.PERSONAL,
                    ApprovementStatus.WAIT_CREATE,
                )
                logger.info('Personal ticket created for %s: %s (%s)', login, key, proposal_id)
            except Exception:
                logger.exception(f'Error trying to create personal ticket for {login}')
                raise


def renew_persons_tickets(
        author_login: str,
        proposal_context: ProposalContext,
        logins: List[str]) -> None:
    """
    Восстанавливает ранее удалённые персональные тикеты по указанным логинам от имени author_login либо автора заявки
    """
    proposal_id = proposal_context.proposal_id

    with log_context(proposal_id=proposal_id, logins=logins):
        for login in logins:
            logger.info('Going to renew personal ticket for %s (%s)', login, proposal_id)
            try:
                personal_ticket_ctl = PersonTicket(proposal_context, login)
                key = personal_ticket_ctl.renew_ticket(author_login=author_login)
                update_approvement(
                    proposal_context,
                    key,
                    ApprovementTicketType.PERSONAL,
                    ApprovementStatus.WAIT_CREATE,
                )
                logger.info('Personal ticket renewed for %s: %s (%s)', login, key, proposal_id)
            except Exception:
                logger.exception(f'Error trying to renew personal ticket for {login}')


def update_persons_tickets(
        author_login: str,
        proposal_context: ProposalContext,
        logins: List[str]) -> None:
    """
    Обновляет контент существующих персональных тикетов по указанным логинам
    кроме полей из `PersonTicket.FROZEN_FIELDS`
    """
    proposal_id = proposal_context.proposal_id
    with log_context(proposal_id=proposal_id, logins=logins):
        for login in logins:
            try:
                logger.info('Going to update personal ticket for %s (%s)', login, proposal_id)
                personal_ticket_ctl = PersonTicket(proposal_context, login)
                key = personal_ticket_ctl.update_ticket(author_login=author_login)
                update_approvement(
                    proposal_context,
                    key,
                    ApprovementTicketType.PERSONAL,
                    ApprovementStatus.WAIT_RERUN,
                )
                logger.info('Personal ticket updated for %s: %s (%s)', login, key, proposal_id)
            except Exception:
                logger.exception(
                    'Error trying to update personal ticket for %s in proposal %s',
                    login, proposal_id
                )


def delete_persons_tickets(
        author_login: str,
        proposal_context: ProposalContext,
        logins: List[str]) -> None:
    """
    Закрывает (пока нет) персональные тикеты, перемещая их в deleted_persons в теле заявки
    Оставляет коментарий в тикете
    """
    proposal_id = proposal_context.proposal_id
    with log_context(proposal_id=proposal_id, logins=logins):
        for login in logins:
            try:
                logger.info('Going to delete personal ticket for %s (%s)', login, proposal_id)
                personal_ticket_ctl = PersonTicket(proposal_context, login)
                ticket_key = personal_ticket_ctl.ticket_key
                personal_ticket_ctl.delete_from_proposal(author_login=author_login)
                update_approvement(
                    proposal_context,
                    ticket_key,
                    ApprovementTicketType.PERSONAL,
                    ApprovementStatus.WAIT_DELETE,
                )
                logger.info('Personal ticket %s for %s was deleted from %s', ticket_key, login, proposal_id)
            except Exception:
                logger.exception('Error trying to delete person\'s %s ticket from proposal %s', login, proposal_id)


def move_persons_tickets_to_r15n(
        author_login: str,
        proposal_context: ProposalContext,
        logins: List[str]) -> None:
    """
    Закрывает (пока нет) персональные тикеты, перемещая их в deleted_persons в теле заявки
    Оставляет комментарий в тикете
    """
    proposal_id = proposal_context.proposal_id
    r15n_ticket_key = proposal_context.proposal.restructurisation_ticket
    if not r15n_ticket_key:
        raise ValueError(
            'Error trying to move %s to restructurisation while no r15n ticket exists in %s',
            logins, proposal_id
        )

    with log_context(proposal_id=proposal_id, logins=logins, restructurisation=r15n_ticket_key):
        for login in logins:
            try:
                logger.info('Going to move personal ticket for %s (%s) to restructurisation', login, proposal_id)
                personal_ticket_ctl = PersonTicket(proposal_context, login)
                ticket_key = personal_ticket_ctl.ticket_key
                personal_ticket_ctl.move_to_r15n(
                    author_login=author_login,
                    r15n_ticket_key=r15n_ticket_key,
                )
                logger.info(
                    'Personal ticket %s for %s was moved to restructurisation %s from %s',
                    ticket_key, login, r15n_ticket_key, proposal_id,
                )
            except Exception:
                logger.exception(
                    'Error trying to move person\'s %s@ ticket to r15n from proposal %s',
                    login,
                    proposal_id,
                )


@app.task
def create_restructurisation_ticket(proposal_context: ProposalContext, author_login: str = None):
    proposal_id = proposal_context.proposal_id

    with log_context(proposal_id=proposal_id):
        logger.info('Creating restructurisation ticket for %s', proposal_id)

        if proposal_context.proposal_object['tickets'].get('department_linked_ticket'):
            ticket_ctl = RestructurisationLinkedTicket(proposal_context)
        else:
            ticket_ctl = RestructurisationTicket(proposal_context)

        key = ticket_ctl.create_ticket(author_login)
        update_approvement(
            proposal_context,
            key,
            ApprovementTicketType.RESTRUCTURISATION,
            ApprovementStatus.WAIT_CREATE,
        )

        logger.info('Restructurisation ticket created %s (%s)', key, proposal_id)


@app.task
def create_headcount_ticket(proposal_context: ProposalContext, author_login: str = None):
    proposal_id = proposal_context.proposal_id
    with log_context(proposal_id=proposal_id):
        ticket_ctl = HeadcountTicket(proposal_context)
        key = ticket_ctl.create_ticket(author_login)
        update_approvement(
            proposal_context,
            key,
            ApprovementTicketType.HEADCOUNT,
            ApprovementStatus.WAIT_CREATE,
        )
        logger.info('Headcount ticket created %s (%s)', key, proposal_id)


@app.task
def create_value_stream_ticket(proposal_context: ProposalContext, author_login: str = None):
    proposal_id = proposal_context.proposal_id
    with log_context(proposal_id=proposal_id):
        ticket_ctl = ValueStreamTicket(proposal_context)
        key = ticket_ctl.create_ticket(author_login)
        logger.info('Value stream ticket created %s (%s)', key, proposal_id)


def update_restructurisation_ticket(author_login: str, proposal_context: ProposalContext = None) -> None:
    with log_context(proposal_id=proposal_context.proposal_id):
        if proposal_context.proposal_object['tickets'].get('department_linked_ticket'):
            ticket_ctl = RestructurisationLinkedTicket(proposal_context)
        else:
            ticket_ctl = RestructurisationTicket(proposal_context)

        updated = ticket_ctl.update_ticket(author_login)
        if updated:
            update_approvement(
                proposal_context,
                ticket_ctl.ticket_key,
                ApprovementTicketType.RESTRUCTURISATION,
                ApprovementStatus.WAIT_RERUN,
            )
        logger.info(
            'Restructurisation ticket updated %s (%s) by %s',
            ticket_ctl.ticket_key,
            proposal_context.proposal_id,
            author_login,
        )


def update_headcount_ticket(author_login: str, proposal_context: ProposalContext = None) -> None:
    with log_context(proposal_id=proposal_context.proposal_id):
        ticket_ctl = HeadcountTicket(proposal_context)
        updated = ticket_ctl.update_ticket(author_login)
        if updated:
            update_approvement(
                proposal_context,
                ticket_ctl.ticket_key,
                ApprovementTicketType.HEADCOUNT,
                ApprovementStatus.WAIT_RERUN,
            )
        logger.info(
            'Headcount ticket updated %s (%s) by %s',
            ticket_ctl.ticket_key,
            proposal_context.proposal_id,
            author_login,
        )


def update_value_stream_ticket(author_login: str, proposal_context: ProposalContext = None) -> None:
    with log_context(proposal_id=proposal_context.proposal_id):
        ticket_ctl = ValueStreamTicket(proposal_context)
        ticket_ctl.update_ticket(author_login)
        logger.info(
            'Value stream ticket updated %s (%s) by %s',
            ticket_ctl.ticket_key,
            proposal_context.proposal_id,
            author_login,
        )


@app.task
def delete_restructurisation_ticket(author_login: str, proposal_id: str, comment_text: str):
    _delete_restructurisation_ticket(author_login=author_login, proposal_id=proposal_id, comment_text=comment_text)


def _delete_restructurisation_ticket(
        author_login: str,
        proposal_context: ProposalContext = None,
        proposal_id: str = '',
        comment_text: str = '') -> None:
    """При удалении заявки в тикет реструктуризации просто пишем коммент"""

    proposal_id = proposal_context.proposal_id if proposal_context else proposal_id
    with log_context(proposal_id=proposal_id):
        if proposal_context:
            if proposal_context.proposal_object['tickets'].get('department_linked_ticket'):
                ticket_ctl = RestructurisationLinkedTicket(proposal_context)
            else:
                ticket_ctl = RestructurisationTicket(proposal_context)
        else:
            ticket_ctl = RestructurisationTicket.from_proposal_id(proposal_id)
            proposal_context = ProposalContext.from_proposal_id(proposal_id)
        ticket_key = ticket_ctl.ticket_key

        ticket_ctl.delete_ticket(
            author_login,
            comment_text=comment_text or f'Заявка {proposal_id} удалена кем:{author_login}'
        )

        update_approvement(
            proposal_context,
            ticket_key,
            ApprovementTicketType.RESTRUCTURISATION,
            ApprovementStatus.WAIT_DELETE,
        )

        logger.info(
            'Restructurisation ticket deleted %s (%s) by %s',
            ticket_ctl.ticket_key,
            proposal_id,
            author_login,
        )


@app.task
def delete_headcount_ticket(author_login: str, proposal_id: str, comment_text: str):
    _delete_headcount_ticket(author_login=author_login, proposal_id=proposal_id, comment_text=comment_text)


def _delete_headcount_ticket(
        author_login: str,
        proposal_context: ProposalContext = None,
        proposal_id: str = '',
        comment_text: str = '') -> None:
    """При удалении заявки в тикет headcount просто пишем коммент"""

    proposal_id = proposal_context.proposal_id if proposal_context else proposal_id
    with log_context(proposal_id=proposal_id):
        if not proposal_context:
            proposal_context = ProposalContext.from_proposal_id(proposal_id)
        ticket_ctl = HeadcountTicket(proposal_context)
        ticket_key = ticket_ctl.ticket_key

        ticket_ctl.delete_ticket(
            author_login,
            comment_text=comment_text or f'Заявка {proposal_id} удалена кем:{author_login}'
        )

        update_approvement(
            proposal_context,
            ticket_key,
            ApprovementTicketType.RESTRUCTURISATION,
            ApprovementStatus.WAIT_DELETE,
        )

        logger.info(
            'Headcount ticket deleted %s (%s) by %s',
            ticket_ctl.ticket_key,
            proposal_id,
            author_login,
        )


@app.task
def delete_value_stream_ticket(author_login: str, proposal_id: str, comment_text: str):
    _delete_value_stream_ticket(author_login=author_login, proposal_id=proposal_id, comment_text=comment_text)


def _delete_value_stream_ticket(
        author_login: str,
        proposal_context: ProposalContext = None,
        proposal_id: str = '',
        comment_text: str = '') -> None:
    """При удалении заявки в тикет valuestream просто пишем коммент"""

    proposal_id = proposal_context.proposal_id if proposal_context else proposal_id
    with log_context(proposal_id=proposal_id):
        if proposal_context:
            ticket_ctl = ValueStreamTicket(proposal_context)
        else:
            context = ProposalContext.from_proposal_id(proposal_id)
            ticket_ctl = ValueStreamTicket(context)

        ticket_ctl.delete_ticket(
            author_login,
            comment_text=comment_text or f'Заявка {proposal_id} удалена кем:{author_login}'
        )

        logger.info(
            'Value stream ticket deleted %s (%s) by %s',
            ticket_ctl.ticket_key,
            proposal_id,
            author_login,
        )


@app.task
def close_person_tickets(
    author_login: str,
    proposal_id: str = '',
    comment_text: str = '',
):
    with log_context(proposal_id=proposal_id):
        logger.info('Notifying persons tickets about closed proposal')
        proposal_context = ProposalContext.from_proposal_id(proposal_id, author=Staff.objects.get(login=author_login))

        for person_act in proposal_context.proposal.person_action_objs:
            ticket_ctl = PersonTicket(proposal_context, person_act.login)
            if not ticket_ctl.ticket_key:
                logger.info('Person action has no ticket')

            ticket_ctl.add_comment_if_has_ticket(
                comment_text or f'Заявка {proposal_id} удалена кем:{author_login}'
            )
            # trying to run task here leads to SEGFAULT in tests.
            # approvement will be eventually consistent due to update_approvements_consistency task
            proposal_context.proposal.update_approvement(
                ticket_ctl.ticket_key,
                ApprovementTicketType.PERSONAL,
                status=ApprovementStatus.WAIT_DELETE,
            )


@app.task
class CreateProposalTicketsTask(app.Task):
    """
    Сразу создаёт тикет реструктуризации
    И ставит задачу отложенного создания персональных тикетов
    """

    def run(self, proposal_id, author_login=None):
        with log_context(task_name='CreateProposalTicketsTask', proposal_id=proposal_id, author_login=author_login):
            self.run_execution(proposal_id, author_login=author_login)

    def run_execution(self, proposal_id, author_login=None):
        logger.info('Creating tickets for %s (%s)', proposal_id, author_login)
        proposal_metadata = ProposalMetadata.objects.get(proposal_id=proposal_id)
        proposal_context = ProposalContext.from_proposal_id(proposal_id)
        ticket_dispatcher = ProposalTicketDispatcher(proposal_context)

        if ticket_dispatcher.is_r15n_ticket_needed:
            # пока создаём тикет р-ции в том же потоке как и департаментный, чтобы из персональных ссылаться на него
            create_restructurisation_ticket(proposal_context=proposal_context)

        if ticket_dispatcher.is_person_tickets_needed:
            # отложено создаём персональные тикеты
            ProposalTasks.schedule_ordered_task(
                proposal_metadata.id,
                create_persons_tickets,
                {'proposal_id': proposal_id},
            )

        if ticket_dispatcher.is_headcount_ticket_needed:
            create_headcount_ticket(proposal_context=proposal_context)

        if ticket_dispatcher.is_value_stream_ticket_needed:
            create_value_stream_ticket(proposal_context=proposal_context)

        logger.info('Tickets for %s created', proposal_id)


def create_proposal_tickets(proposal_id: str) -> None:
    proposal_metadata = ProposalMetadata.objects.get(proposal_id=proposal_id)
    ProposalTasks.schedule_ordered_task(
        proposal_metadata.id,
        CreateProposalTicketsTask,
        {'proposal_id': proposal_id},
    )


@app.task
def approve_ready_proposals():
    if waffle.switch_is_active('disable_auto_approve_proposal'):
        return
    today = datetime.datetime.combine(datetime.date.today(), datetime.datetime.min.time())
    half_year_ago = today - relativedelta(months=6)
    ctls = [*ProposalCtl.filter({
        'apply_at': {'$lte': today, '$gte': half_year_ago},
        'finished': None,
        'approvements': {'$exists': True, '$not': {'$size': 0}},
        'approvements.uuid': {'$exists': True, '$ne': None},
        'approvements.staff_uid': {'$exists': True},
    })]
    not_applied_proposal_ids = set(
        ProposalMetadata.objects
        .values_list('proposal_id', flat=True)
        .filter(proposal_id__in=(it.proposal_id for it in ctls))
        .filter(applied_at__isnull=True)
    )
    ctls_to_process = (
        ctl for ctl in ctls
        if ctl.proposal_id in not_applied_proposal_ids
    )
    for ctl in ctls_to_process:
        dispatcher = ProposalTicketDispatcher(ProposalContext(ctl))
        if not dispatcher.is_all_tickets_created:
            continue
        tickets_with_approvements = {
            approvement.ticket_key
            for approvement in ctl.get_approvements()
            if approvement.uuid
        }
        ticket_keys = set([
            dispatcher.tickets.get('headcount'),
            dispatcher.tickets.get('restructurisation'),
            *dispatcher.tickets.get('persons', {}).values(),
        ])
        ticket_keys -= {None, ''}
        if not ticket_keys.issubset(tickets_with_approvements):
            # auto-execute proposals only with auto-created approvements
            continue
        tickets = startrek.issues.get_issues(ticket_keys)
        is_approved = tickets and all(
            issue.approvementStatus == 'Согласовано'
            for issue in tickets
        )
        if not is_approved:
            continue

        with atomic():
            ctl.enable_auto_apply()
            ctl.lock()
            proposal_metadata = ProposalMetadata.objects.get(proposal_id=ctl.proposal_id)
            ProposalTasks.schedule_ordered_task(
                proposal_metadata.id,
                ExecuteProposal,
                {'proposal_id': ctl.proposal_id},
            )


@app.task
def update_approvements_consistency():
    query = {'approvements.status': {'$ne': ApprovementStatus.OK.value, '$exists': True}}
    for ctl in ProposalCtl.filter(query):
        with atomic():
            proposal_meta_id = (
                ProposalMetadata.objects
                .values_list('id', flat=True)
                .filter(proposal_id=ctl.proposal_id)
                .first()
            )
            ProposalTasks.schedule_ordered_task(
                proposal_meta_id,
                callable_or_task=update_approvements_state,
                kwargs={'proposal_id': ctl.proposal_id},
            )


@atomic
def update_approvement(
    proposal_ctx: ProposalContext,
    ticket_key: str,
    ticket_type: ApprovementTicketType,
    status: ApprovementStatus,
):
    logger.info(
        f'Update approvement for proposal {proposal_ctx.proposal_id}'
        f' {ticket_key} {ticket_type.value} {status.value}'
    )
    proposal_ctx.proposal.update_approvement(
        ticket_key,
        ticket_type,
        status=status,
    )
    proposal_meta_id = (
        ProposalMetadata.objects
        .values_list('id', flat=True)
        .filter(proposal_id=proposal_ctx.proposal_id)
        .first()
    )
    ProposalTasks.schedule_ordered_task(
        proposal_meta_id,
        callable_or_task=update_approvements_state,
        kwargs={'proposal_id': proposal_ctx.proposal_id},
    )


@app.task
def update_approvements_state(proposal_id: str = None):
    proposal_context = ProposalContext.from_proposal_id(proposal_id)
    for approvement in proposal_context.proposal.get_approvements():
        to_stop = (ApprovementStatus.WAIT_RERUN.value, ApprovementStatus.WAIT_DELETE.value)
        if approvement.status in to_stop:
            stopped = approvement_ctl.stop(approvement.ticket_key, proposal_context)
            if not stopped:
                continue  # Failed to stop approvement. Trying later
        if approvement.status == ApprovementStatus.WAIT_DELETE.value:
            proposal_context.proposal.update_approvement(
                approvement.ticket_key,
                approvement.ticket_type,
                status=ApprovementStatus.OK,
                remove_uuid=True,
            )
        if approvement.status == ApprovementStatus.WAIT_RERUN.value:
            approvement.staff_uid = proposal_context.proposal.update_approvement(
                approvement.ticket_key,
                approvement.ticket_type,
                status=ApprovementStatus.WAIT_CREATE,
                remove_uuid=True,
            )
            approvement.status = ApprovementStatus.WAIT_CREATE.value
        if approvement.status == ApprovementStatus.WAIT_CREATE.value:
            uuid = approvement_ctl.create(
                proposal_context,
                approvement.ticket_key,
                approvement.ticket_type,
                approvement.staff_uid,
            )
            proposal_context.proposal.update_approvement(
                approvement.ticket_key,
                approvement.ticket_type,
                uuid,
                ApprovementStatus.OK,
                remove_uuid=not uuid,
            )


@app.task
class SyncProposalTickets(celery.Task):
    """
    Таска, которая приводит тикеты, связанные с заявкой в актуальное состояние.
    Это не такой же LockedTask как в `staff.lib.tasks`
    Задача этого лока не отвалиться при занятом локе, а подождать и поретраиться.
    """
    LOCK_BLOCK_TIMEOUT = 5  # секунд повисим в надежде что лок освободится
    RETRY_COUNTDOWN = 20
    MAX_RETRIES = 10

    @classmethod
    def get_lock_name(cls, proposal_id: str) -> str:
        return f'{cls.__name__}__{proposal_id}'

    @classmethod
    def is_running(cls, proposal_id: str) -> bool:
        return lock_manager.lock(cls.get_lock_name(proposal_id)).check_acquired()

    def run(
        self,
        author_login: str,
        proposal_id: str,
        proposal_diff: Dict[str, Any],
        updated_logins: List[str],
        deleted_logins: List[str]
    ) -> None:
        with lock_manager.lock(
                self.get_lock_name(proposal_id),
                block=True,
                block_timeout=self.LOCK_BLOCK_TIMEOUT,
                timeout=LOCK_TRANSACTION_TIMEOUT,
        ) as lock_aquired:
            if lock_aquired:
                with log_context(
                    proposal_id=proposal_id,
                    author_login=author_login,
                    updated_logins=updated_logins,
                    retries=self.request.retries,
                ):
                    return self._locked_run(author_login, proposal_id, proposal_diff, updated_logins, deleted_logins)
            else:
                # Если тикеты уже апдейтятся, то поретраимся
                raise self.retry(countdown=self.RETRY_COUNTDOWN, max_retries=self.MAX_RETRIES)

    def _locked_run(
            self,
            author_login: str,
            proposal_id: str,
            proposal_diff: Dict[str, Any],
            updated_logins: List[str],
            deleted_logins: List[str]) -> None:
        """
        Обновляет состояние всех трёх видов тикетов: департанментный, реструктуризации, персональные
        Создаёт\\восстанавливает тикеты по добавленным в заявку людям.
        Удаляет из заявки тикеты удалённых людей
        `deleted_logins` пока не используем,
            пока нет ясности всегда ли он совпадает с old_state_personal_tickets - new_state_personal_tickets
            Это зависит от того будем ли удалять тикеты людей, переехавших в реструктуризацию.
        """
        logger.info('SyncProposalTickets started for %s', proposal_id)

        self.proposal_context = ProposalContext.from_proposal_id(proposal_id)
        self.ticket_dispatcher = ProposalTicketDispatcher(self.proposal_context)

        self.update_r15n_ticket(author_login=author_login)
        self.update_headcount_ticket(author_login=author_login)
        self.update_value_stream_ticket(author_login=author_login)
        self.update_personal_tickets(author_login, updated_logins, deleted_logins)

        logger.info('SyncProposalTickets finished for %s', proposal_id)

    def update_headcount_ticket(self, author_login: str) -> None:
        headcount_ticket_key = self.ticket_dispatcher.tickets['headcount']
        if headcount_ticket_key and self.ticket_dispatcher.is_headcount_ticket_needed:
            update_headcount_ticket(author_login, proposal_context=self.proposal_context)
        elif headcount_ticket_key and not self.ticket_dispatcher.is_headcount_ticket_needed:
            _delete_headcount_ticket(
                author_login,
                proposal_context=self.proposal_context,
                comment_text='Заявка изменена, этот тикет больше не нужен',
            )
        elif not headcount_ticket_key and self.ticket_dispatcher.is_headcount_ticket_needed:
            create_headcount_ticket(proposal_context=self.proposal_context, author_login=author_login)

    def update_value_stream_ticket(self, author_login: str) -> None:
        value_stream_ticket_key = self.ticket_dispatcher.tickets['value_stream']
        if value_stream_ticket_key and self.ticket_dispatcher.is_value_stream_ticket_needed:
            update_value_stream_ticket(author_login, proposal_context=self.proposal_context)
        elif value_stream_ticket_key and not self.ticket_dispatcher.is_value_stream_ticket_needed:
            _delete_value_stream_ticket(
                author_login,
                proposal_context=self.proposal_context,
                comment_text='Заявка изменена, этот тикет больше не нужен',
            )
        elif not value_stream_ticket_key and self.ticket_dispatcher.is_value_stream_ticket_needed:
            create_value_stream_ticket(proposal_context=self.proposal_context, author_login=author_login)

    def update_r15n_ticket(self, author_login: str) -> None:
        r15n_ticket_key = self.ticket_dispatcher.tickets['restructurisation']
        if r15n_ticket_key and self.ticket_dispatcher.is_r15n_ticket_needed:
            update_restructurisation_ticket(author_login, proposal_context=self.proposal_context)
        elif r15n_ticket_key and not self.ticket_dispatcher.is_r15n_ticket_needed:
            _delete_restructurisation_ticket(
                author_login,
                proposal_context=self.proposal_context,
                comment_text='Заявка изменена, этот тикет больше не нужен',
            )
        elif not r15n_ticket_key and self.ticket_dispatcher.is_r15n_ticket_needed:
            create_restructurisation_ticket(proposal_context=self.proposal_context, author_login=author_login)

    def update_personal_tickets(self, author_login: str, updated_logins: List[str], deleted_logins: List[str]) -> None:
        # login: key
        login_to_ticket: Dict[str, str] = self.ticket_dispatcher.tickets['persons']
        # login: key
        deleted_login_to_ticket: Dict[str, str] = self.ticket_dispatcher.tickets.get('deleted_persons', {})

        new_state_personal_tickets: Set[str] = {act['login'] for act in self.ticket_dispatcher.personal}
        new_r15n_personal_tickets: Set[str] = {act['login'] for act in self.ticket_dispatcher.restructurisation}

        old_state_personal_tickets = set(login_to_ticket)
        old_state_deleted_tickets = set(deleted_login_to_ticket)

        to_create = list(new_state_personal_tickets - old_state_personal_tickets - old_state_deleted_tickets)  # создать
        to_renew = list(new_state_personal_tickets & old_state_deleted_tickets)  # восстановить из удалённых
        # обновить состояние
        to_update = list(new_state_personal_tickets & old_state_personal_tickets & set(updated_logins))
        to_r15n = list(old_state_personal_tickets & new_r15n_personal_tickets)  # удалить с комментом о р-ции
        to_delete = list(old_state_personal_tickets - new_state_personal_tickets - set(to_r15n))  # удалить

        logger.info(
            'SyncProposalTickets: start personal tickets management; '
            'author: %s, to_create: %s, to_renew: %s to_update: %s, to_r15n: %s, to_delete: %s',
            author_login, to_create, to_renew, to_update, to_r15n, to_delete,
        )

        if to_create:
            create_persons_tickets(author_login, proposal_context=self.proposal_context, logins=to_create)
        if to_renew:
            renew_persons_tickets(author_login, proposal_context=self.proposal_context, logins=to_renew)
        if to_update:
            update_persons_tickets(author_login, proposal_context=self.proposal_context, logins=to_update)
        if to_delete:
            delete_persons_tickets(author_login, proposal_context=self.proposal_context, logins=to_delete)
        if to_r15n:
            move_persons_tickets_to_r15n(author_login, proposal_context=self.proposal_context, logins=to_r15n)


@app.task
def retry_all_tasks():
    from staff.proposal.controllers.proposal_tasks import ProposalTasks
    ProposalTasks.retry_all()


@app.task
def delete_proposal_task(proposal_id: int, login: str):
    with log_context(proposal_id=proposal_id):
        proposal_metadata = ProposalMetadata.objects.get(id=proposal_id)
        logger.info('Deleting proposal %s', proposal_metadata.proposal_id)
        proposal_ctl = ProposalCtl(proposal_id=proposal_metadata.proposal_id)
        proposal_ctl.delete()

        if proposal_ctl.restructurisation_ticket:  # пока легаси
            ProposalTasks.schedule_ordered_task(
                proposal_id,
                callable_or_task=delete_restructurisation_ticket,
                kwargs={
                    'author_login': login,
                    'proposal_id': proposal_metadata.proposal_id,
                    'comment_text': 'Заявка {0} удалена'.format(proposal_id),
                },
            )

        if proposal_ctl.value_stream_ticket:
            ProposalTasks.schedule_ordered_task(
                proposal_id,
                callable_or_task=delete_value_stream_ticket,
                kwargs={
                    'author_login': login,
                    'proposal_id': proposal_metadata.proposal_id,
                    'comment_text': 'Заявка {0} удалена'.format(proposal_id),
                },
            )

        if proposal_ctl.headcount_ticket:
            ProposalTasks.schedule_ordered_task(
                proposal_id,
                callable_or_task=delete_headcount_ticket,
                kwargs={
                    'author_login': login,
                    'proposal_id': proposal_metadata.proposal_id,
                    'comment_text': 'Заявка {0} удалена'.format(proposal_id),
                },
            )

        ProposalTasks.schedule_ordered_task(
            proposal_id,
            callable_or_task=close_person_tickets,
            kwargs={'author_login': login, 'proposal_id': proposal_metadata.proposal_id}
        )

        logger.info('Deleted proposal %s', proposal_metadata.proposal_id)
