import logging
from copy import deepcopy
from itertools import chain
from typing import Any, Dict, Iterator, List, Set, Union

import attr

from staff.lib.log import log_context
from staff.lib.models.departments_chain import get_departments_tree
from staff.lib.models.roles_chain import get_grouped_chiefs_by_departments
from staff.map.models import Office
from staff.oebs.models.job import Job
from staff.person.models import Organization, Staff
from staff.proposal.controllers.headcount import HeadcountDataCtl
from staff.proposal.controllers.vacancy import VacancyDataCtl
from staff.proposal.models import DepartmentAttrs, Grade

from staff.departments.controllers import proposal
from staff.departments.controllers.instance_getters import InstanceGetter
from staff.departments.controllers.proposal_action import (
    Action,
    DepartmentSection,
    PersonSection,
    PersonSectionsSet,
    is_creating,
    is_deleting,
    is_dep_action,
    is_headcount_action,
    is_moving,
    is_person_action,
    is_vacancy_action,
    ordered_actions,
)
from staff.departments.controllers.tickets import helpers
from staff.departments.edit.constants import HR_CONTROLLED_DEPARTMENT_ROOTS
from staff.departments.models import Department, Vacancy, HRProduct, ValuestreamRoles, Geography
from staff.departments.models.headcount import HeadcountPosition


logger = logging.getLogger(__name__)


NONDEPARTMENT_FIELDS = ('name.hr_type', )  # Поля заявки, которые не относятся к подразделению.


class ProposalTicketCtlError(Exception):
    pass


def _is_from_maternity_leave(person_action):
    department_section = person_action.get(PersonSection.DEPARTMENT.value)
    return department_section and department_section['from_maternity_leave']


def _is_transfer_without_budget(person_action):
    department_section = person_action.get(PersonSection.DEPARTMENT.value)
    return department_section and not department_section['with_budget']


def collect_vacancies(proposal):
    """Возвращает id всех вакансий из заявки"""
    if 'actions' not in proposal.get('vacancies', {}):
        return []

    ids = [
        action['vacancy_id']
        for action in proposal['vacancies']['actions']
    ]
    return {_f for _f in ids if _f}


def prefetch_vacancies(proposal_object: Dict[str, Any]) -> Dict[int, Dict[str, Any]]:
    proposal_vacancies_ids = collect_vacancies(proposal_object)
    vacancies_data = (
        Vacancy.objects
        .filter(id__in=proposal_vacancies_ids)  # is published ??
        .values(*VacancyDataCtl.vacancy_fields)
    )
    return {v['id']: v for v in vacancies_data}


def collect_headcounts(proposal):
    """Возвращает code всех бюджетных позиций из заявки"""
    if 'actions' not in proposal.get('headcounts', {}):
        return []

    ids = [
        action['headcount_code']
        for action in proposal['headcounts']['actions']
    ]
    return {_f for _f in ids if _f}


def prefetch_headcounts(proposal_object: Dict[str, Any]) -> Dict[int, Dict[str, Any]]:
    proposal_headcount_codes = collect_headcounts(proposal_object)
    headcounts_data = (
        HeadcountPosition.objects
        .filter(code__in=proposal_headcount_codes)
        .values(*HeadcountDataCtl.headcount_fields)
    )
    return {
        h['code']: h
        for h in headcounts_data
    }


def prefetch_persons(proposal_object: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
    """Структура вида
    {
        login: {
            instance: <staff instance>
            field: value,
            ...
        },
        ...
    }
    """
    person_fields = (
        'id', 'login',
        'first_name', 'last_name',
        'department_id',  # 'department__url',
        'office_id', 'position',
    )
    staff_qs = (
        Staff.objects
        .filter(id__in=proposal.collect_person_ids_from_proposal(proposal_object, from_tickets=True))
        .prefetch_related('department')
    )

    persons_data = {
        p.login: {'instance': p}
        for p in staff_qs
    }
    for login, data in persons_data.items():
        instance: Staff = data['instance']
        for field in person_fields:
            data[field] = getattr(instance, field)
        data['department__url'] = instance.department.url

    return persons_data


@attr.s(auto_attribs=True, frozen=True)
class HRProductData:
    hr_product_slug: str
    hr_product_head: str


class ProposalContext(object):
    """
    Класс для кеширования связанных с заявкой сущностей
    умеет отвечать на вопросы, необходимые контроллерам тикетов.
    """
    def __init__(self, proposal_ctl: proposal.ProposalCtl) -> None:
        self.proposal = proposal_ctl
        self.proposal_object = self.proposal.proposal_object
        self.proposal_id = proposal_ctl.proposal_id

        self.all_actions = list(
            ordered_actions(
                deepcopy(
                    list(self.proposal.all_actions)
                ),
                with_not_executable_actions=True,
            )
        )

        self.get_department = InstanceGetter(Department.objects, lookup_unique_field='id')
        self.get_person = InstanceGetter(Staff.objects, lookup_unique_field='id')

        self.department_actions = list(self._filter_actions(filters=[is_dep_action]))
        self.person_actions = list(self._filter_actions(filters=[is_person_action]))
        self.vacancy_actions = list(self._filter_actions(filters=[is_vacancy_action]))
        self.headcount_actions = list(self._filter_actions(filters=[is_headcount_action]))
        self.clean_nondepartment_fields()

        self.new_department_names = {
            act['fake_id']: act[DepartmentSection.NAME.value]['name']
            for act in self.department_actions
            if is_creating(act)
        }

        # lazy cached properties
        self._wage_system_data: Dict[int]
        self._vacancies_data: Dict[int, Dict] = {}
        self._headcounts_data: Dict[int, Dict] = {}
        self._persons_data: Dict[str, Dict] = {}
        self._dep_data: Dict[Union[int, str], Dict] = {}
        self._dep_chains: Dict[Union[int, str], helpers.DepartmentChain] = {}

        self._official_positions: Dict[int, str] = {}
        self._positions: Dict[int, Job] = {}
        self._offices: Dict[int, Office] = {}
        self._organizations: Dict[int, Organization] = {}
        self._grades: Dict[int, Grade] = {}
        self._new_value_streams: Dict[str, str or None] or None = None
        self._existing_value_streams: Dict[int, str or None] or None = None
        self._existing_geographies: Dict[int, str or None] or None = None
        self._involved_hc_positions_codes: Set[int] or None = None
        self._hr_products: Dict[str, HRProductData] or None = None
        self._hr_products_data: Dict[str, HRProduct] = {}
        self._geography_data: Dict[str, Geography] = {}
        self._person_budget_positions: Dict[str, int] or None = None

        self.department_ids_all: Set[int] = set(
            proposal.collect_department_ids(
                self.proposal_object,
                parents=True,
                new_parents=True,
                person_deps=True,
                from_tickets=True,
                vacancy_deps=True,
                headcount_deps=True,
            )
        )

        self.dispatcher = ProposalTicketDispatcher(self)

        r15n_logins = {act['login'] for act in self.dispatcher.restructurisation}
        self.dep_ids_from_personal_changes_for_r15n = proposal.collect_department_ids_from_person_actions(
            [act for act in self.proposal_object['persons']['actions'] if act['login'] in r15n_logins],
            actual_deps=True,
            new_deps=True,
        )

        self.department_ids_for_r15n: Set[int] = set(
            chain(
                proposal.collect_department_ids(
                    self.proposal_object,
                    parents=True,
                    new_parents=True,
                    vacancy_deps=True,
                    headcount_deps=True,
                ),
                self.dep_ids_from_personal_changes_for_r15n,
            )
        )

        self.department_attrs_with_chains: Set[DepartmentAttrs] = set(
            chain.from_iterable(helpers.department_attrs.get_all(dep_id) for dep_id in self.department_ids_for_r15n)
        ) - {None}

        self.department_attrs_without_chains: Set[DepartmentAttrs] = set(
            helpers.department_attrs.get_one(dep_id) for dep_id in self.department_ids_for_r15n
        ) - {None}

        self.department_analysts = set(
            helpers.department_attrs.get_analyst(self.get_department(dep_id))
            for dep_id in self.department_ids_for_r15n
        ) - {None}

        self.dep_names = self.get_dep_names()

    @classmethod
    def from_proposal_id(cls, proposal_id, author=None) -> 'ProposalContext':
        proposal_ctl = proposal.ProposalCtl(proposal_id, author=author)
        return cls(proposal_ctl)

    @classmethod
    def from_proposal_ctl(cls, proposal_ctl) -> 'ProposalContext':
        return cls(proposal_ctl)

    def clean_nondepartment_fields(self) -> None:
        for field in NONDEPARTMENT_FIELDS:
            fieldset, fieldname = field.split('.')
            for action in self.department_actions:
                if fieldset in action:
                    action[fieldset].pop(fieldname, None)

    def _prefetch_departments(self) -> Dict[Union[int, str], Dict[str, Any]]:
        """Структура вида
        {
            dep_id: {'instance': <dep_instance>, 'id': dep_id, 'chain': [{}, {}]},
            dep_url: {'instance': <dep_instance>, 'id': dep_id, 'chain': [{}, {}]},
            fake_id: {'ru': '', 'en': ''},
        }
        """
        self.get_department(self.department_ids_all)  # зафетчит все одним запросом.
        dep_data = {
            dep_id: {'chain': dep_chain, 'id': dep_id, 'instance': self.get_department(dep_id)}
            for dep_id, dep_chain in get_departments_tree(self.department_ids_all).items()
        }

        dep_data.update(
            # создаваемые подразделения пока характеризуем только названиями.
            {
                act['fake_id']: {
                    'ru': act[DepartmentSection.NAME.value]['name'],
                    'en': act[DepartmentSection.NAME.value]['name_en']
                }
                for act in self.department_actions if is_creating(act)
            }
        )

        by_urls = {
            details['instance'].url: details
            for details in dep_data.values()
            if 'instance' in details
        }
        dep_data.update(by_urls)

        return dep_data

    def prefetch_offices(self, proposal_object) -> Dict[int, Office]:
        person_actions = proposal_object['persons']['actions']
        office_ids = [
            act[PersonSection.OFFICE.value]['office']
            for act in person_actions
            if PersonSection.OFFICE.value in act
        ]
        office_ids.extend(p['office_id'] for p in self.persons_data.values())
        return Office.objects.in_bulk(id_list=office_ids)

    @property
    def renaming_department_actions(self) -> Iterator[Action]:
        for action in self.department_actions:
            name_changing = (
                DepartmentSection.NAME.value in action and
                action['id'] and
                action[DepartmentSection.NAME.value]['name'] != self.get_department(action['id']).name
            )

            if name_changing:
                yield action

    def get_creating_department_names(self):
        for action in filter(is_creating, self.department_actions):
            yield action[DepartmentSection.NAME.value]['name']
        for action in self.renaming_department_actions:
            yield action[DepartmentSection.NAME.value]['name']

    def get_deleting_department_names(self):
        for action in filter(is_deleting, self.department_actions):
            yield self.get_department(action['id']).name
        for action in self.renaming_department_actions:
            yield self.get_department(action['id']).name

    def get_moving_department_names(self):
        for action in filter(is_moving, self.department_actions):
            yield self.get_department(action['id']).name

    def _filter_actions(self, filters=None):
        filters = filters or []
        for act in self.all_actions:
            if any(f(act) for f in filters):
                yield act

    def get_dep_names(self) -> Dict[Union[str, int], str]:
        """id или fake_id -> название"""
        dep_names = {}
        for action in self.department_actions:
            if not is_dep_action(action):
                continue
            if DepartmentSection.NAME.value in action:
                dep_names[action['id'] or action['fake_id']] = action[DepartmentSection.NAME.value]['name']
            elif action['id']:
                dep_names[action['id']] = self.get_department(action['id']).name
        return dep_names

    @property
    def vacancies_data(self) -> Dict[int, Dict[str, Any]]:
        if not self._vacancies_data:
            self._vacancies_data = prefetch_vacancies(self.proposal_object)
        return self._vacancies_data

    @property
    def headcounts_data(self) -> Dict[int, Dict[str, Any]]:
        if not self._headcounts_data:
            self._headcounts_data = prefetch_headcounts(self.proposal_object)
        return self._headcounts_data

    @property
    def hr_product_data(self) -> Dict[str, HRProduct]:
        if not self._hr_products_data:
            self._hr_products_data = {
                product.value_stream.url: product
                for product in HRProduct.objects.filter(value_stream__isnull=False).select_related('value_stream')
            }
        return self._hr_products_data

    @property
    def geography_data(self) -> Dict[str, Geography]:
        if not self._geography_data:
            self._geography_data = {
                geography.url: geography
                for geography in Department.geography.active()
            }
        return self._geography_data

    @property
    def persons_data(self) -> Dict[str, Dict[str, Any]]:
        if not self._persons_data:
            self._persons_data = prefetch_persons(self.proposal_object)
        return self._persons_data

    @property
    def dep_data(self) -> Dict[Union[int, str], Dict[str, Any]]:
        if not self._dep_data:
            self._dep_data = self._prefetch_departments()
        return self._dep_data

    @property
    def dep_chains(self) -> Dict[Union[int, str], helpers.DepartmentChain]:
        if not self._dep_chains:
            chains = helpers.get_departments_chains(
                [dep_id for dep_id in self.dep_data if isinstance(dep_id, int)]
            )

            by_url = {}
            for key, chain_data in chains.items():
                by_url[chain_data['urls'][-1]] = chain_data
            chains.update(by_url)

            self._dep_chains = chains

        return self._dep_chains

    @property
    def positions(self):
        if not self._positions:
            job_ids = []
            for act in self.proposal.person_actions:
                position_section = act.get(PersonSection.POSITION.value)
                if position_section:
                    job_ids.append(position_section['position_legal'])
            self._positions = Job.objects.in_bulk(id_list=job_ids)
        return self._positions

    @property
    def official_positions(self):
        if not self._official_positions:
            self._official_positions = helpers.get_official_positions(
                [pd['id'] for pd in self.persons_data.values()]
            )
        return self._official_positions

    @property
    def offices(self) -> Dict[int, Office]:
        if not self._offices:
            self._offices = self.prefetch_offices(self.proposal_object)
        return self._offices

    @property
    def organizations(self) -> Dict[int, Dict[str, Any]]:
        if not self._organizations:
            orgs = (
                Organization.objects
                .filter(intranet_status=1)
                .values('id', 'name', 'name_en', 'st_translation_id')
            )
            self._organizations = {o['id']: o for o in orgs}
        return self._organizations

    @property
    def grades(self) -> Dict[str, Dict[str, Any]]:
        if not self._grades:
            grades = Grade.objects.values()
            self._grades = {g['level']: g for g in grades}
        return self._grades

    @property
    def person_budget_positions(self) -> Dict[str, int]:
        if self._person_budget_positions is not None:
            return self._person_budget_positions

        result = dict(
            HeadcountPosition.objects
            .filter(current_person__login__in=self.persons_data.keys())
            .order_by('main_assignment')
            .values_list('current_person__login', 'code')
        )
        self._person_budget_positions = result
        return self._person_budget_positions

    @property
    def involved_hc_positions_codes(self) -> Set[int]:
        if self._involved_hc_positions_codes is not None:
            return self._involved_hc_positions_codes

        result = list(self.headcounts_data.keys())
        result += [vacancy_data['headcount_position_code'] for vacancy_data in self.vacancies_data.values()]

        result += list(self.person_budget_positions.values())

        self._involved_hc_positions_codes = set(result)
        return self._involved_hc_positions_codes

    @property
    def new_value_streams(self) -> Dict[str, str or None]:
        """Action id to value stream"""
        if self._new_value_streams is not None:
            return self._new_value_streams

        result = {
            action['action_id']: action.get('value_stream', {}).get('value_stream')
            for action in self.person_actions
        }
        result.update({action['action_id']: action.get('value_stream') for action in self.vacancy_actions})
        result.update({action['action_id']: action.get('value_stream') for action in self.headcount_actions})
        self._new_value_streams = result
        return self._new_value_streams

    @property
    def existing_value_streams(self) -> Dict[int, str or None]:
        """Budget position code to value stream"""
        if self._existing_value_streams is not None:
            return self._existing_value_streams

        result = dict(
            HeadcountPosition.objects
            .filter(code__in=self.involved_hc_positions_codes)
            .values_list('code', 'valuestream__url')
        )
        self._existing_value_streams = result
        return self._existing_value_streams

    @property
    def existing_geographies(self) -> Dict[int, str or None]:
        if self._existing_geographies is not None:
            return self._existing_geographies

        geography_oebs_code_to_url_mapping = dict(
            Geography.objects
            .active()
            .values_list('oebs_code', 'department_instance__url')
        )

        headcount_geo_mapping = dict(
            HeadcountPosition.objects
            .filter(code__in=self.involved_hc_positions_codes)
            .values_list('code', 'geo')
        )
        self._existing_geographies = {
            headcount_code: geography_oebs_code_to_url_mapping.get(geography_oebs_code)
            for headcount_code, geography_oebs_code in headcount_geo_mapping.items()
        }
        return self._existing_geographies

    @property
    def hr_products(self) -> Dict[str, HRProductData]:
        if self._hr_products is not None:
            return self._hr_products

        value_stream_head_role = ValuestreamRoles.HEAD.value
        value_stream_fields = ('id', 'code', 'url', 'lft', 'rght', 'tree_id')
        value_streams_list = set(self.existing_value_streams.values()) | set(self.new_value_streams.values())
        value_streams_list.discard(None)
        qs = (
            Department.valuestreams
            .filter(url__in=value_streams_list)
            .values(*value_stream_fields)
        )

        chiefs = get_grouped_chiefs_by_departments(qs, ('login', ), (value_stream_head_role, ))

        result = {
            value_stream['url']: HRProductData(
                hr_product_slug=value_stream['code'],
                hr_product_head=chiefs.get(value_stream['id'], {}).get('staff__login'),
            )
            for value_stream in qs
        }
        self._hr_products = result
        return self._hr_products


class ProposalTicketDispatcher(object):
    """
    Умеет отвечать на вопросы какие тикеты нужны и какие сущности в них должны входить
    Никаких изменений в тело заявки вносить не должен
    """

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

        self.personal: List[Action] = []  # экшены из persons, по которым создаём персональные тикеты
        self.restructurisation: List[Action] = []  # экшены из persons, которые попадут в реструктуризацию

        self.persons = self.context.persons_data
        self.departments = self.context.dep_data
        self.tickets: Dict[str, Any] = self.context.proposal_object['tickets']

        self.run_dispatcher()

    @classmethod
    def from_proposal_id(cls, proposal_id):
        context = ProposalContext.from_proposal_id(proposal_id)
        return cls(context)

    @classmethod
    def from_proposal_ctl(cls, proposal_ctl):
        context = ProposalContext.from_proposal_ctl(proposal_ctl)
        return cls(context)

    def is_hr_fields_changing(self, person_action: Action) -> bool:
        """меняется (зарплата ИЛИ ставка ИЛИ система оплаты труда ИЛИ грейд ИЛИ организация)"""
        hr_fields_sections = PersonSectionsSet.from_person_section(
            PersonSection.SALARY,
            PersonSection.GRADE,
            PersonSection.ORGANIZATION
        )
        return PersonSectionsSet(*person_action.keys()).has_any_section_from(hr_fields_sections)

    def is_moving_or_positioning(self, person_action: Action) -> bool:
        """Меняется (подразделение ИЛИ кадровая должность ИЛИ офис)"""
        login = person_action['login']
        sections_in_action = PersonSectionsSet(*person_action.keys())
        moving_or_positioning_sections = PersonSectionsSet.from_person_section(
            PersonSection.DEPARTMENT,
            PersonSection.POSITION,
            PersonSection.OFFICE,
        )

        if not sections_in_action.has_any_section_from(moving_or_positioning_sections):
            return False

        if sections_in_action.only_section_from(moving_or_positioning_sections, is_the=PersonSection.DEPARTMENT):
            if person_action[PersonSection.DEPARTMENT.value]['department'] == self.persons[login]['department__url']:
                return False  # Возможно перемещение в свой же департамент в рамках выхода из декрета

        assert sections_in_action.has_any_section_from(moving_or_positioning_sections)
        return True

    def is_from_maternity_or_without_budget(self, person_action: Action) -> bool:
        """выход из декрета == да ИЛИ перевод без бюджета == да"""
        return (
            _is_from_maternity_leave(person_action)
            or _is_transfer_without_budget(person_action)
        )

    def condition1(self, person_action: Action) -> bool:
        return (
            self.is_hr_fields_changing(person_action)
            or self.is_from_maternity_or_without_budget(person_action)
        )

    def condition2(self, person_action: Action) -> bool:
        """
        Mеняется (подразделение ИЛИ кадровая должность ИЛИ офис)
        И не меняется (зарплата ИЛИ ставка ИЛИ система оплаты труда ИЛИ грейд ИЛИ организация)
        """
        return (
            self.is_moving_or_positioning(person_action)
            and not self.is_hr_fields_changing(person_action)
        )

    def condition3(self):
        """
        Меняется структура синхронизируемых с OEBS веток
        ИЛИ меняется название синхронизируемого с OEBS подразделения
        """
        for department_action in self.context.department_actions:
            if department_action['__department_chain__'][0] in HR_CONTROLLED_DEPARTMENT_ROOTS:
                return True
            return False

    def changes_only_value_stream(self, person_action: Action) -> bool:
        sections_in_action = PersonSectionsSet(*person_action.keys())
        return sections_in_action.has_only_section(PersonSection.VALUE_STREAM)

    def run_dispatcher(self) -> None:
        """
        Заполняет self.personal и self.restructurisation экшенами сотрудников,
        которые должны попадать в соотв. тикеты
        """
        self.personal = []
        self.restructurisation = []

        cond2_actions = [act for act in self.context.person_actions if self.condition2(act)]
        cond2_actions_count = len(cond2_actions)

        logger.debug('Going to dispatch proposal %s', self.context.proposal_id)
        with log_context(method='run_dispatcher', proposal_id=self.context.proposal_id):
            for person_action in self.context.person_actions:
                if not self.changes_only_value_stream(person_action):
                    if self.condition1(person_action):
                        self.personal.append(person_action)
                        logger.debug('Person: %s personal condition1', person_action['login'])
                    elif cond2_actions_count >= 5:
                        logger.debug('Person: %s r15n condition2 (>= 5)', person_action['login'])
                        self.restructurisation.append(person_action)
                    elif self.condition3():
                        logger.debug('Person: %s r15n condition3 (< 5)', person_action['login'])
                        self.restructurisation.append(person_action)
                    else:
                        logger.debug('Person: %s personal (no conditions)', person_action['login'])
                        self.personal.append(person_action)

            logger.info(
                '%s Dispatched. Personal: %s, r15n: %s',
                self.context.proposal_id,
                [a['login'] for a in self.personal],
                [a['login'] for a in self.restructurisation],
            )

    @property
    def r15n_vacancy_actions(self) -> Iterator[Action]:
        for action in self.context.proposal.vacancy_actions:
            yield action

    @property
    def value_stream_vacancy_actions(self) -> Iterator[Action]:
        for action in self.context.proposal.vacancy_actions:
            if action.get('value_stream') or action.get('geography'):
                yield action

    @property
    def headcount_actions(self) -> Iterator[Action]:
        for action in self.context.proposal.headcount_actions:
            yield action

    @property
    def value_stream_headcount_actions(self) -> Iterator[Action]:
        for action in self.context.proposal.headcount_actions:
            if action.get('value_stream') or action.get('geography'):
                yield action

    @property
    def value_stream_person_actions(self) -> Iterator[Action]:
        for action in self.context.proposal.person_actions:
            if action.get('value_stream', {}).get('value_stream') or action.get('geography', {}).get('geography'):
                yield action

    @property
    def is_department_ticket_needed(self) -> bool:  # todo: delete
        return bool(
            set(self.context.proposal_object['root_departments']) &
            set(HR_CONTROLLED_DEPARTMENT_ROOTS.keys())
        )

    @property
    def is_r15n_ticket_needed(self) -> bool:
        return bool(
            set(self.context.proposal_object['root_departments']) &
            set(HR_CONTROLLED_DEPARTMENT_ROOTS.keys())
        ) or any(self.restructurisation)

    @property
    def is_person_tickets_needed(self) -> bool:
        return len(self.personal) > 0

    @staticmethod
    def _headcount_or_vacancy_action_does_not_change_department(action) -> bool:
        result = not bool(action.get('department'))
        return result

    @property
    def is_headcount_ticket_needed(self) -> bool:
        for action in chain(self.headcount_actions, self.r15n_vacancy_actions):
            if not self._headcount_or_vacancy_action_does_not_change_department(action):
                return True

        return False

    def _has_section(self, section_name: str) -> bool:
        if any(action.get(section_name, {}).get(section_name) for action in self.context.person_actions):
            return True

        if any(action.get(section_name) for action in self.headcount_actions):
            return True

        if any(action.get(section_name) for action in self.r15n_vacancy_actions):
            return True

        return False

    @property
    def is_value_stream_ticket_needed(self) -> bool:
        if self._has_section('value_stream') or self._has_section('geography'):
            return True

        return False

    @property
    def is_all_tickets_created(self) -> bool:
        return (
            (len(self.tickets['persons']) == len(self.personal)) and
            (not self.is_r15n_ticket_needed or bool(self.tickets.get('restructurisation'))) and
            (not self.is_headcount_ticket_needed or bool(self.tickets.get('headcount'))) and
            (not self.is_value_stream_ticket_needed or bool(self.tickets.get('value_stream')))
        )
