import logging
from datetime import datetime
from itertools import chain
from typing import Any, Dict, List, Optional, Set, Tuple

from staff.lib import waffle
from django.conf import settings
from django.template import Context, loader

from staff.lib.models.roles_chain import get_hrbp_by_departments
from staff.lib.startrek.issues import create_issue, get_issue
from staff.lib.startrek.queues import get_queue

from staff.departments.controllers.instance_getters import get_kind
from staff.departments.controllers.proposal import DepartmentDataCtl
from staff.departments.controllers.proposal_action import (
    Action,
    DepartmentSection,
    PersonSection,
    is_deleting,
    is_creating,
    is_moving,
    is_person_moving,
)
from staff.departments.controllers.tickets import helpers
from staff.departments.controllers.tickets.base import (
    ProposalContext,
    ProposalTicketCtlError,
    ProposalTicketDispatcher,
)
from staff.departments.models import Department


logger = logging.getLogger(__name__)


class RestructurisationTicket:
    # Поля тикета, не обновляемые при изменении заявки
    FROZEN_FIELDS = 'assignee', 'followers', 'access'
    TICKET_TEMPLATE = 'startrek/restructurisation_ticket.html'
    queue_id = settings.PROPOSAL_QUEUE_ID

    def __init__(self, proposal_context: ProposalContext):
        self.context = proposal_context

        self.ticket_key: str = self.get_ticket_key()
        self._components: Dict[str, int] = {}
        self._ticket_dispatcher = None
        self._logins = None  # логины людей, согласование изменений по которым попадает в этот тикет
        self._analysts = None  # логины аналитиков, в чьих владениях происходят изменения этой заявки

    @classmethod
    def from_proposal_id(cls, proposal_id: str) -> 'RestructurisationTicket':
        context = ProposalContext.from_proposal_id(proposal_id)
        if context.proposal_object['tickets'].get('department_linked_ticket'):
            return RestructurisationLinkedTicket(context)
        return cls(context)

    @classmethod
    def from_proposal_ctl(cls, proposal_ctl) -> 'RestructurisationTicket':
        context = ProposalContext(proposal_ctl)
        if context.proposal_object['tickets'].get('department_linked_ticket'):
            return RestructurisationLinkedTicket(context)
        return cls(context)

    def get_component_id_by_name(self, component_name: str) -> Optional[int]:
        if not self._components:
            st_queue = get_queue(queue=self.queue_id)
            self._components = {
                comp.name: comp.id
                for comp in st_queue.components
            }
        return self._components.get(component_name)

    @property
    def issue(self):
        if not self.ticket_key:
            raise ProposalTicketCtlError(f'Proposal {self.context.proposal_id} has no ticket')

        return get_issue(key=self.ticket_key)

    @property
    def ticket_dispatcher(self):
        if self._ticket_dispatcher is None:
            self._ticket_dispatcher = ProposalTicketDispatcher(self.context)
        return self._ticket_dispatcher

    @property
    def logins(self) -> Set[str]:
        if self._logins is None:
            self._logins = {act['login'] for act in self.ticket_dispatcher.restructurisation}
        return self._logins

    @property
    def analysts(self) -> Set[str]:
        """
        Список логинов аналитиков, в чьих владениях происходят изменения из заявки
            за вычетом тех, согласование изменений по которым попали в заявку.
        Сначала идут аналитики отдающей стороны в случае если выполняется перенос подразделения или перевод сотрудника.
        """
        if self._analysts is None:
            moving_dep_analysts = set()
            moving_person_analysts = set()
            self._analysts = self.context.department_analysts

            if len(self._analysts) > 1:
                moving_dep_analysts = {
                    helpers.department_attrs.get_analyst(self.context.get_department(act['id']))
                    for act in self.context.department_actions
                    if is_moving(act)
                } - {None}
                moving_person_analysts = {
                    helpers.department_attrs.get_analyst(
                        self.context.get_department(self.context.persons_data[act['login']]['instance'].department_id)
                    )
                    for act in self.context.person_actions
                    if is_person_moving(act)
                } - {None}
                moving_dep_analysts.difference_update(moving_person_analysts)

                self._analysts.difference_update(moving_dep_analysts)
                self._analysts.difference_update(moving_person_analysts)

            self._analysts = {
                *moving_dep_analysts,
                *moving_person_analysts,
                *self._get_headcount_analysts(),
                *self._analysts
            }

        return self._analysts

    def get_ticket_key(self) -> str:
        return self.context.proposal_object['tickets'].get('restructurisation')

    def get_apply_date(self):
        result = self.context.proposal_object['apply_at']
        if isinstance(result, datetime):
            result = result.date()
        return result

    def generate_ticket_context(self, enumerate_entries=True):
        def flat(action):
            """Убирает уровень fieldset'ов, помещает все значения в {'value': value}"""
            result = {'delete': action['delete']}
            for section in DepartmentSection:
                if section.value in action:
                    result.update({
                        key: {'value': value}
                        for key, value in action[section.value].items()
                    })
            return result

        def hydrate(action_data):
            """заменяем айдишники моделями"""

            def hydrate_structure(structure, instance_getter):
                if isinstance(structure, list):
                    return [hydrate_structure(p, instance_getter) for p in structure]
                if isinstance(structure, dict):
                    return {
                        s_key: hydrate_structure(s_data, instance_getter)
                        for s_key, s_data in structure.items()
                    }
                return instance_getter(structure)

            def hydrate_person(person_data):
                return hydrate_structure(person_data, instance_getter=self.context.get_person)

            def hydrate_kind(kind_data):
                return hydrate_structure(kind_data, instance_getter=get_kind)

            def hydrate_department(department_data):
                return hydrate_structure(
                    department_data,
                    instance_getter=lambda department_id: (
                        self.context.new_department_names.get(department_id) or
                        self.context.get_department(department_id)
                    )
                )

            if 'parent' in action_data:
                action_data['parent'] = hydrate_department(action_data['parent'])

            if 'fake_parent' in action_data:
                action_data['fake_parent'] = hydrate_department(action_data['fake_parent'])

            if 'chief' in action_data:
                action_data['chief'] = hydrate_person(action_data['chief'])

            if 'kind' in action_data:
                action_data['kind'] = hydrate_kind(action_data['kind'])

            if 'deputies' in action_data:
                action_data['deputies'] = hydrate_person(action_data['deputies'])

            return action_data

        def add_parent_dep_name(action_data):
            """
            Для создаваемых подразделений добавляет new_dep_name в разделе parent
                в случае когда указан fake_parent
            """
            if 'hierarchy' not in action_data:
                return action_data

            if action_data['hierarchy']['fake_parent']:
                action_data['hierarchy']['new_dep_name'] = (
                    self.context.dep_names[action_data['hierarchy']['fake_parent']]
                )
            return action_data

        # Для секции update строим diff по существующим в базе подразделениям
        diff_data = DepartmentDataCtl.as_diff(self.context.department_actions)

        hydrated_diff = {
            pk: hydrate(action_data)
            for pk, action_data in diff_data.items()
        }

        # костыльно добавляем 'new_dep_name' на один уровень с 'new' и 'old'
        # для тех перемещаемых подразделений, родители которых переименовываются в этой же заявке.
        # Чтобы рендерить в их уже с новым именем.
        for dep_id, dep_diff in hydrated_diff.items():
            if 'parent' in dep_diff:
                parent_diff_dict = dep_diff['parent']

                new_parent_new_name = (
                    isinstance(parent_diff_dict['new'], Department) and
                    parent_diff_dict['new'].id in hydrated_diff and
                    'name' in hydrated_diff[parent_diff_dict['new'].id] and
                    hydrated_diff[parent_diff_dict['new'].id]['name']['new']
                )
                if new_parent_new_name:
                    parent_diff_dict['new_dep_name'] = new_parent_new_name

        apply_at_hr_date = datetime.strptime(
            self.context.proposal_object['apply_at_hr'], '%Y-%m-%d'
        ).date()

        # context_data - данные для рендера с actions в правильном порядке

        context_data = {
            'proposal_id': self.context.proposal_id,
            'settings': settings,
            'description': self.context.proposal_object['description'],
            'apply_at': self.get_apply_date(),
            'apply_at_hr': apply_at_hr_date,
            'departments': [
                {
                    'id': act['id'],
                    'number': num if enumerate_entries else None,
                    'fake_id': act['fake_id'],
                    'department_chain': act.get('__department_chain__', []),
                    'department': self.context.get_department(act['id']),
                    'update': hydrated_diff.get(act['id']),
                    'create': hydrate(flat(add_parent_dep_name(act))) if is_creating(act) else None,
                    'delete': hydrate(flat(act)) if is_deleting(act) else None,
                } for num, act in enumerate(self.context.department_actions, 1)
            ],
            'persons': [
                self.prepare_person_action_ticket_context(action)
                for action in self.ticket_dispatcher.restructurisation
            ],
            'vacancies': [
                self.prepare_vacancy_action_ticket_context(action)
                for action in self.ticket_dispatcher.r15n_vacancy_actions
            ]
        }

        return context_data

    def prepare_person_action_ticket_context(self, action_data: Action) -> Dict[str, Any]:
        login = action_data['login']
        person_data = self.context.persons_data[login]
        person_context = {
            'login': login,
            'first_name': person_data['first_name'],
            'last_name': person_data['last_name'],
            'department': {
                'old': self.context.dep_data[person_data['department_id']]['instance'],
                'new': '',
            },
            'position': {
                'old': person_data['position'],
                'new': '',
            },
            'office': {
                'old': self.context.offices[person_data['office_id']].name,
                'new': '',
            },
            'geography': None,
        }

        department_section = action_data.get(PersonSection.DEPARTMENT.value)
        if department_section:
            new_dep_key = (
                department_section['department']
                or department_section['fake_department']
            )
            new_dep_details = self.context.dep_data[new_dep_key]
            new_dep_details = (
                new_dep_details['instance']
                if 'instance' in new_dep_details
                else new_dep_details['ru']
            )
            person_context[PersonSection.DEPARTMENT.value]['new'] = new_dep_details

        position_section = action_data.get(PersonSection.POSITION.value)
        if position_section:
            position_legal = position_section['position_legal']
            name_position = helpers.get_new_official_position(position_legal)
            person_context[PersonSection.POSITION.value]['new'] = name_position

        office_section = action_data.get(PersonSection.OFFICE.value)
        if office_section:
            new_office_id = office_section['office']
            person_context[PersonSection.OFFICE.value]['new'] = self.context.offices[new_office_id].name

        geography_section = action_data.get(PersonSection.GEOGRAPHY.value)
        if geography_section:
            person_context['geography'] = self.context.geography_data.get(geography_section['geography'])

        return person_context

    def _put_hr_product_to_context(
        self,
        context_object: dict,
        headcount_position_code: int,
        action_data: Action,
    ) -> None:
        old_value_stream = self.context.existing_value_streams.get(headcount_position_code)
        new_value_stream = action_data.get('value_stream')

        if old_value_stream:
            hr_product = self.context.hr_product_data.get(old_value_stream)
            context_object['old_hr_product'] = hr_product and hr_product.service_id

        if new_value_stream:
            hr_product = self.context.hr_product_data.get(new_value_stream)
            context_object['new_hr_product'] = hr_product and hr_product.service_id

    def _put_geography_to_context(
        self,
        context_object: dict,
        headcount_position_code: int,
        action_data: Action,
    ) -> None:
        old_geography = self.context.existing_geographies.get(headcount_position_code)
        new_geography = action_data.get('geography')

        if old_geography:
            context_object['old_geography'] = self.context.geography_data.get(old_geography)

        if new_geography:
            context_object['new_geography'] = self.context.geography_data.get(new_geography)

    def prepare_vacancy_action_ticket_context(self, action_data: Action) -> Dict[str, Any]:
        vacancy_id = action_data['vacancy_id']
        vacancy_data = self.context.vacancies_data[vacancy_id]
        headcount_code = vacancy_data['headcount_position_code']
        vacancy_context = {
            'vacancy_id': vacancy_id,
            'name': vacancy_data['name'],
            'status': vacancy_data['status'],
            'headcount_position_code': headcount_code,
            'ticket': vacancy_data['ticket'],
            'department': {
                'old': {'url': vacancy_data['department__url']},
                'new': {},
            },
            'new_hr_product': None,
            'old_hr_product': None,
            'new_geography': None,
            'old_geography': None,
        }

        existing_department = action_data.get('department')
        fake_department = action_data.get('fake_department')

        if existing_department:
            vacancy_context['department']['new']['url'] = existing_department
        elif fake_department:
            new_dep_details = self.context.dep_data[fake_department]
            vacancy_context['department']['new'] = new_dep_details['ru']

        self._put_hr_product_to_context(vacancy_context, headcount_code, action_data)
        self._put_geography_to_context(vacancy_context, headcount_code, action_data)

        return vacancy_context

    def generate_ticket_params(self) -> Tuple[Dict, Set[str]]:
        components, missing_components = self.get_components()

        return {
            'assignee': self.get_assignee(),

            'staffDate': self.context.proposal_object['apply_at'].strftime('%Y-%m-%d'),
            'followers': self.get_followers(),
            'access': self.get_accessors(),
            'components': components,
            'analytics': list(self.analysts),

            'toCreate': '; '.join(self.context.get_creating_department_names()),
            'toMove': '; '.join(self.context.get_moving_department_names()),
            'toDelete': '; '.join(self.context.get_deleting_department_names()),
        }, missing_components

    def delete_ticket(self, author_login: str, comment_text: str) -> None:
        """
        Пока что просто пишет комментарий в тикет и удаляет ключ тикета из заявки
        """
        self.add_comment(author_login, comment_text=comment_text)
        tickets = self.context.proposal_object['tickets']
        tickets['deleted_restructurisation'] = tickets['restructurisation']
        self.ticket_key = ''
        self.update_ticket_key_in_proposal()

    def create_ticket(self, author_login=None) -> str:
        """
        Создаёт тикет в стартреке в очереди SALARY (TSALARY для тестинга).
        Примеры ключей в ticket_params: summary, description, assignee, tags, followers
        Пересохраняет заявку, добавив в неё ключ тикета
        Возвращает строку с ключом тикета
        """
        if self.ticket_key:
            raise ProposalTicketCtlError(self.ticket_key)
        old_key = self.context.proposal_object['tickets'].get('deleted_restructurisation')
        if old_key:
            self._restore_ticket(old_key, author_login)
        else:
            self._create_ticket()
        self.update_ticket_key_in_proposal()
        return self.ticket_key

    def _create_ticket(self) -> None:
        ticket_params, missing_components = self.generate_ticket_params()

        ticket_description = self.get_description()
        self.ticket_key = create_issue(
            createdBy=self.context.proposal.author.login,
            queue=self.queue_id,
            summary=self.get_summary(),
            description=ticket_description,
            unique=self.get_unique(),
            **ticket_params
        ).key

        if missing_components:
            comment_text = (f'Некоторые компоненты не добавлены в тикет, '
                            f'т.к. отсутствуют в очереди: '
                            f'{", ".join(missing_components)}.')
            self.add_comment('robot-staff', comment_text=comment_text)

    def _restore_ticket(self, old_key, author_login):
        self.ticket_key = old_key
        self.update_ticket(author_login, 'Тикет восстановлен.\n')
        logger.info(
            'Restructurisation ticket restored %s (%s) by %s',
            self.ticket_key,
            self.context.proposal_id,
            author_login,
        )

    def add_comment(self, author_login: str, comment_text: str) -> None:
        full_comment_text = f'@{author_login}:\n<[{comment_text}]>'.strip()

        try:
            self.issue.comments.create(text=full_comment_text)
        except Exception as e:
            logger.error(
                'Error trying to add comment to ticket %s. Comment text: %s',
                self.ticket_key,
                full_comment_text,
            )
            raise e

    def update_ticket(self, author_login: str, comment_text: str = '') -> bool:
        """
        Обновляет тикет и добавляет комментарий, если передан comment_text
        Все делаем от имени robot-staff, в коммент добавляется логин автора.
        """

        # об отсутствующих компонентах в очереди пишем только при создании тикета
        ticket_params, _ = self.generate_ticket_params()
        ticket_description = self.get_description()
        for field_name in self.FROZEN_FIELDS:
            ticket_params.pop(field_name, None)

        updated = False
        try:
            issue = self.issue
            cur_desc = issue.description
            if cur_desc != ticket_description:
                comment_text = f'{comment_text}Условия были изменены. <{{Предыдущие условия:\n{cur_desc} }}>'
                ticket_params['description'] = ticket_description
                updated = True
            issue.update(**ticket_params)
            if comment_text:
                self.add_comment(author_login, comment_text=comment_text)
            logger.info('Ticket %s (%s) updated', self.ticket_key, self.context.proposal_id)
        except Exception as e:
            logger.exception(
                'Error trying to update ticket %s. additional fields: %s',
                self.ticket_key,
                list(ticket_params.keys())
            )
            raise e
        return updated

    def update_ticket_key_in_proposal(self):
        self.context.proposal.update_tickets(restructurisation_ticket=self.ticket_key)
        self.context.proposal.save()

    def get_description(self):
        ticket_context = self.generate_ticket_context()
        proposal_template = loader.get_template(self.TICKET_TEMPLATE)
        proposal_description = proposal_template.render(Context(ticket_context))

        return proposal_description

    def get_summary(self):
        return 'Заявка на реструктуризацию'

    def get_assignee(self):
        """Возвращает логин исполнителя тикета"""
        if 0 < len(self.analysts) < 3:
            return next(iter(self.analysts))
        else:
            return helpers.department_attrs.default.analyst.login

    def get_followers(self) -> List[str]:
        """
        Возвращает список логинов сотрудников, проставляемых в поле 'наблюдатели'
            на основании hr-партнёров и по данным DepartmentAttrs
        Если 3 и более аналитика, наблюдателей не проставляем.
        """
        if len(self.analysts) > 2:
            return []

        hr_followers: Set[str] = self._get_hr_followers()
        other_followers: Set[str] = self._get_followers_by_attributes()
        follower_logins = hr_followers | other_followers
        return list(follower_logins - self.logins)

    def get_accessors(self) -> List[str]:
        """
        Возвращает список логинов сотрудников, проставляемых в поле 'доступ'
        на основании правил бизнес логики и данных DepartmentAttrs
        Если 3 и более аналитика, доступы не проставляем.
        """
        if len(self.analysts) > 2:
            logger.info('Analyst count is %s, skipping accessors', len(self.analysts))
            return []

        department_attrs = list(self.context.department_attrs_without_chains)  # здесь учтены новые parent`ы
        access_persons = set(chain.from_iterable(attr.ticket_access.all() for attr in department_attrs))
        accessors = {person.login for person in access_persons}
        current_accessors = set()

        if self.ticket_key:
            current_accessors = {x.login for x in self.issue.access}
            logger.info('Current accessors %s', current_accessors)

        return list(accessors.union(current_accessors) - self.logins) or list(
            helpers.department_attrs.default.ticket_access
            .values_list('login', flat=True)
        )

    def get_components(self) -> Tuple[List[int], Set[str]]:
        """
        Возвращает кортеж из списка id компонентов очереди для этого тикета реструктуризации
        и списка названий компонент, которые не найдены в очереди Стартрека.

        Для компонент нужно учитывать все службы вплоть до Яндекса (department_full_attrs)
        Предохранитель. Компонента Руководство не должна попасть в тикет.
        Если 3 и более аналитика, компоненты не проставляем.
        """
        if waffle.switch_is_active('disable_proposal_st_components'):
            return [], set()

        if len(self.analysts) > 2:
            return [], set()

        all_budget_tags = set(
            attr.budget_tag
            for attr in self.context.department_attrs_with_chains
            if attr.budget_tag
        )
        all_budget_tags.discard('Руководство')

        component_ids = set()
        for tag in all_budget_tags.copy():
            component_id = self.get_component_id_by_name(tag)
            if component_id:
                component_ids.add(component_id)
                all_budget_tags.remove(tag)

        # В all_budget_tags останутся теги, для которых нет компоненты в очереди
        return list(component_ids), all_budget_tags

    def get_unique(self):
        return f'proposal/{self.context.proposal_id}/restructurisation'

    def _get_hr_followers(self) -> Set[str]:
        """Возвращает set логинов hr-партнёров, проставляемых в поле 'наблюдатели'"""
        if waffle.switch_is_active('proposal_block_hrpartners_followers_in_r15n'):
            return set()

        dep_instances = [
            self.context.dep_data[dep_id]['instance']
            for dep_id in self.context.department_ids_for_r15n
        ]

        hr_partners = get_hrbp_by_departments(department_list=dep_instances, fields=['login'])
        hr_followers = {hr['login'] for hr in hr_partners}
        return hr_followers

    def _get_budget_notify_all_followers(self, department_id: int) -> Set[str]:
        attr = helpers.department_attrs.get_one(department_id)

        if attr:
            return {person.login for person in attr.budget_notify.all()}

        logger.warning('DepartmentAttrs not found for department %s, using default', department_id)
        return set()

    def _get_followers_by_attributes(self) -> Set[str]:
        """
        Возвращает set логинов соотрудников, проставляемых в поле 'наблюдатели'
        на основании правил бизнес логики и данных DepartmentAttrs
        """
        department_attrs = list(self.context.department_attrs_without_chains)  # здесь учтены новые parent'ы
        follower_persons = set(
            chain.from_iterable(attr.budget_notify.all() for attr in department_attrs)
        )
        followers = {person.login for person in follower_persons}

        return (followers - self.logins) or set(
            helpers.department_attrs.default.budget_notify
            .values_list('login', flat=True)
        )

    def _get_headcount_analysts(self) -> Set[str]:
        return set()

    def __repr__(self):
        return f'<RestructurisationTicket `{self.ticket_key}` for {self.context.proposal_id}>'


class RestructurisationLinkedTicket(RestructurisationTicket):
    """
    Тикет реструктуризации, который не был создан с заявкой, а был прилинкован через поле link_to_ticket
    Никакие изменения в тикет не вносит, только добавляет комментрий с новым состоянием заявки.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.linked_ticket_key = self.context.proposal_object['tickets']['department_linked_ticket']
        assert self.linked_ticket_key

    def _create_ticket(self):
        self.ticket_key = self.linked_ticket_key
        ticket_description = self.get_description()
        self.add_comment(
            author_login=self.context.proposal.author.login,
            comment_text=f'Создана дополнительная заявка\n{ticket_description}'
        )

    def _restore_ticket(self, old_key, author_login):
        self.ticket_key = old_key
        ticket_description = self.get_description()
        self.add_comment(author_login, f'Тикет к дополнительной заявке± восстановлен.\n{ticket_description}')
        logger.info(
            'RestructurisationLinkedTicket ticket restored %s (%s) by %s',
            self.ticket_key,
            self.context.proposal_id,
            author_login,
        )

    def delete_ticket(self, author_login: str, comment_text: str) -> None:
        return super().delete_ticket(
            author_login=author_login,
            comment_text=f'Дополнительная заявка удалена кем:{author_login}\n{comment_text}'
        )

    def update_ticket(self, author_login: str, comment_text: str = '') -> bool:
        """
        Добавляет комментарий с новым описанием тикета.
        """
        ticket_description = self.get_description()
        comment_text = f'Изменена дополнительная заявка.\n{comment_text}\n{ticket_description}'
        try:
            if comment_text:
                self.add_comment(author_login, comment_text=comment_text)
            logger.info('Ticket %s (%s) updated', self.ticket_key, self.context.proposal_id)
        except Exception as e:
            logger.exception(
                'Error trying to update ticket %s.',
                self.ticket_key,
            )
            raise e
        return True

    def __repr__(self):
        return f'<RestructurisationLinkedTicket `{self.ticket_key}` for {self.context.proposal_id}>'
