import json
from collections import defaultdict

from django.conf import settings
from django.contrib.postgres.search import SearchVectorField
from django.db import models
from django.utils.functional import cached_property
from intranet.femida.src.vacancies.choices import VACANCY_STATUSES
from model_utils.fields import AutoCreatedField
from model_utils.models import TimeStampedModel

from intranet.femida.src.core.db.fields import StartrekIssueKeyField
from intranet.femida.src.core.models import City, Location, WorkMode
from intranet.femida.src.permissions.managers.vacancy import VacancyPermManager
from intranet.femida.src.permissions.managers.vacancy_group import VacancyGroupPermManager
from intranet.femida.src.professions.models import Profession, ProfessionalSphere
from intranet.femida.src.skills.models import Skill
from intranet.femida.src.staff.models import Department, ValueStream, Geography, Office
from intranet.femida.src.vacancies.managers import VacancyManager
from intranet.femida.src.wf.models import WFModelMixin

from . import choices


class Vacancy(WFModelMixin, TimeStampedModel):

    WIKI_FIELDS_MAP = {
        'publication_content': 'formatted_publication_content',
    }

    unsafe = VacancyManager()
    objects = VacancyPermManager()

    # Общие поля
    name = models.CharField(max_length=255)
    status = models.CharField(max_length=32, choices=choices.VACANCY_STATUSES, default='draft')
    resolution = models.CharField(
        max_length=32,
        choices=choices.VACANCY_RESOLUTIONS,
        default='',
        blank=True,
    )
    is_approved = models.NullBooleanField(default=False)
    created_by = models.ForeignKey(
        to=settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
        related_name='created_vacancies',
        null=True,
    )
    cities = models.ManyToManyField(
        to=City,
        through='vacancies.VacancyCity',
        related_name='vacancies',
    )
    offices = models.ManyToManyField(
        to=Office,
        through='vacancies.VacancyOffice',
        related_name='vacancies',
    )
    work_mode = models.ManyToManyField(
        to=WorkMode,
        through='vacancies.VacancyWorkMode',
        related_name='vacancies',
    )
    locations = models.ManyToManyField(
        to=Location,
        through='vacancies.VacancyLocation',
        related_name='vacancies',
    )
    deadline = models.DateField(null=True, blank=True)
    department = models.ForeignKey(
        to=Department,
        on_delete=models.CASCADE,
        related_name='vacancies',
        blank=True,
        null=True,
    )
    profession = models.ForeignKey(
        to=Profession,
        on_delete=models.CASCADE,
        related_name='vacancies',
        null=True,
        blank=True,
    )
    professional_sphere = models.ForeignKey(
        to=ProfessionalSphere,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
    )
    pro_level_min = models.SmallIntegerField(
        choices=choices.VACANCY_PRO_LEVELS,
        null=True,
        blank=True,
    )
    pro_level_max = models.SmallIntegerField(
        choices=choices.VACANCY_PRO_LEVELS,
        null=True,
        blank=True,
    )
    skills = models.ManyToManyField(
        to=Skill,
        through='vacancies.VacancySkill',
        related_name='vacancies',
    )
    priority = models.CharField(
        max_length=32,
        choices=choices.VACANCY_PRIORITIES,
        default=choices.VACANCY_PRIORITIES.normal,
    )
    type = models.CharField(
        max_length=32,
        choices=choices.VACANCY_TYPES,
        default=choices.VACANCY_TYPES.new,
    )
    abc_services = models.ManyToManyField(
        to='staff.Service',
        related_name='vacancies',
        blank=True,
    )
    value_stream = models.ForeignKey(
        to=ValueStream,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
    )
    geography_international = models.BooleanField(default=False)
    geography = models.ForeignKey(
        to=Geography,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='vacancies',
    )

    # Служебные поля, не участвуют в формах создания/редактирования
    budget_position_id = models.IntegerField(null=True, blank=True)
    bp_transaction_id = models.CharField(
        max_length=64,
        null=True,
        blank=True,
        help_text='ID транзакции в реестре БП',
    )
    startrek_key = StartrekIssueKeyField(null=True, blank=True)
    is_hidden = models.BooleanField(default=False)

    # Поля, актуальные для вакансии на замену (type == replacement)
    instead_of = models.ForeignKey(
        to=settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
        null=True,
        blank=True,
    )
    replacement_reason = models.CharField(
        max_length=32,
        choices=choices.VACANCY_REPLACEMENT_REASONS,
        default='',
        blank=True,
    )

    # Comments
    responsibilities_comment = models.TextField(default='', blank=True)
    additional_qualities_comment = models.TextField(default='', blank=True)
    recommendations_comment = models.TextField(default='', blank=True)
    kpi_comment = models.TextField(default='', blank=True)
    other_comment = models.TextField(default='', blank=True)

    # Данные в json, которые нужно прокинуть при создании тикета в ST
    # в нем context - данные для рендеринга шаблона описания тикета
    # fields - дополнительные поля тикета
    raw_issue_data = models.TextField(default='{}')

    # Внутреннее объявление о вакансии
    publication_title = models.CharField(max_length=255, default='', blank=True)
    publication_content = models.TextField(default='', blank=True)
    formatted_publication_content = models.TextField(default='', blank=True)
    is_published = models.BooleanField(default=False)

    search_vector = SearchVectorField(null=True, blank=True)

    touched_at = models.DateTimeField(
        null=True,
        db_index=True,
        help_text=(
            'Время, когда в последний раз что-то происходило с вакансией, '
            'а также со связанными с ней значимыми объектами'
        ),
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._update_initial()

    @cached_property
    def issue_data(self):
        return json.loads(self.raw_issue_data)

    @cached_property
    def members_by_role(self):
        result = defaultdict(list)

        if 'memberships' in getattr(self, '_prefetched_objects_cache', {}):
            memberships = self.memberships.all()
        else:
            memberships = self.memberships.select_related('member')

        for membership in memberships:
            result[membership.role].append(membership.member)
        return result

    @property
    def hiring_manager(self):
        hiring_managers = self.members_by_role[choices.VACANCY_ROLES.hiring_manager]
        return hiring_managers[0] if hiring_managers else None

    @property
    def head(self):
        heads = self.members_by_role[choices.VACANCY_ROLES.head]
        return heads[0] if heads else None

    @property
    def recruiters(self):
        main_recruiter = self.main_recruiter
        return ([main_recruiter] if main_recruiter else []) + self.researchers

    @property
    def main_recruiter(self):
        main_recruiters = self.members_by_role[choices.VACANCY_ROLES.main_recruiter]
        return main_recruiters[0] if main_recruiters else None

    @property
    def researchers(self):
        """
        Изначально у нас была одна роль recruiters, где были записаны все рекрутеры.
        Сейчас мы выделили из нее отдельную роль main_recruiter.
        Для обеспечения обратной совместимости на фронт мы продолжаем отдавать поле recruiters,
        но в бэке подразумеваем, что это researchers, помощники главного рекрутера.
        Теперь recruiters = main_recruiter + researchers.
        """
        return self.members_by_role[choices.VACANCY_ROLES.recruiter]

    @property
    def responsibles(self):
        return self.members_by_role[choices.VACANCY_ROLES.responsible]

    @property
    def team(self):
        members = {self.hiring_manager, self.head} | set(self.responsibles)
        return list(members - {None})

    @property
    def interviewers(self):
        return self.members_by_role[choices.VACANCY_ROLES.interviewer]

    @property
    def observers(self):
        return self.members_by_role[choices.VACANCY_ROLES.observer]

    @property
    def is_internship(self):
        return self.type == choices.VACANCY_TYPES.internship

    @property
    def is_no_preferred_location(self):
        return len(self.locations) == 0

    def add_membership(self, member, role):
        if not self.memberships.filter(member=member, role=role).exists():
            return self.memberships.create(member=member, role=role)

    def set_main_recruiter(self, recruiter):
        memberships_to_delete = (
            self.memberships
            .filter(role=choices.VACANCY_ROLES.main_recruiter)
            .exclude(member=recruiter)
        )
        memberships_to_delete.delete()
        return self.add_membership(recruiter, choices.VACANCY_ROLES.main_recruiter)

    def add_recruiter(self, recruiter):
        return self.add_membership(recruiter, choices.VACANCY_ROLES.recruiter)

    def add_hiring_manager(self, hiring_manager):
        return self.add_membership(hiring_manager, choices.VACANCY_ROLES.hiring_manager)

    def add_head(self, head):
        return self.add_membership(head, choices.VACANCY_ROLES.head)

    def add_responsible(self, responsible):
        return self.add_membership(responsible, choices.VACANCY_ROLES.responsible)

    def add_interviewer(self, member):
        return self.add_membership(member, choices.VACANCY_ROLES.interviewer)

    def add_observer(self, member):
        return self.add_membership(member, choices.VACANCY_ROLES.observer)

    @property
    def composite_name(self):
        return 'VAC {} {}'.format(self.id, self.name)

    def __str__(self):
        return self.composite_name

    def save(self, *args, **kwargs):
        """
        Создает записи в VacancyHistory, если изменения значимы для OEBS
        """
        is_creation = self.pk is None
        super().save(*args, **kwargs)
        if is_creation:
            VacancyHistory.create_from_vacancy(vacancy=self)
            self._update_initial()
            return

        is_status_changed = self.status != self._initial['status']
        is_bp_changed = self.budget_position_id != self._initial['budget_position_id']
        if is_bp_changed:
            # OEBS способен обработать изменение БП только через фиктивное закрытие вакансии
            # по старой БП и открытие вакансии по новой БП. FEMIDA-5259
            VacancyHistory.create_from_vacancy(
                vacancy=self,
                budget_position_id=self._initial['budget_position_id'],
                status=VACANCY_STATUSES.closed,
            )
        if is_bp_changed or is_status_changed:
            VacancyHistory.create_from_vacancy(vacancy=self)
            self._update_initial()

    def _update_initial(self):
        self._initial = {
            'status': self.status,
            'budget_position_id': self.budget_position_id,
        }

    class Meta:
        default_manager_name = 'unsafe'
        indexes = [
            models.Index(
                fields=['id', 'touched_at'],
                name='vacancy_id_touched_at_idx',
            ),
        ]


def _vacancy_membership_delete_mode(collector, field, sub_objs, using):
    """
    Каскадно удаляет VacancyMembership с открытыми вакансиями или ролью auto_observer.
    В других случаях SET_NULL.
    Использование: передать функцию в ForeignKey(on_delete=...).
    """
    safe_to_delete = (
        models.Q(role=choices.VACANCY_ROLES.auto_observer)
        | ~models.Q(vacancy__status=choices.VACANCY_STATUSES.closed)
    )

    cascade_objs = sub_objs.filter(safe_to_delete)
    setnull_objs = sub_objs.exclude(safe_to_delete)

    models.CASCADE(collector, field, cascade_objs, using)
    models.SET_NULL(collector, field, setnull_objs, using)


class VacancyMembership(TimeStampedModel):

    unsafe = models.Manager()
    objects = VacancyPermManager(perm_prefix='vacancy')

    vacancy = models.ForeignKey(
        to=Vacancy,
        on_delete=models.CASCADE,
        related_name='memberships',
    )
    member = models.ForeignKey(
        to=settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='vacancy_memberships',
    )
    role = models.CharField(max_length=32, choices=choices.VACANCY_ROLES)

    # Это поле нужно, только чтобы каскадно удалять memberships при удалении department_users.
    # Это происходит при синхронизации со staff для автоматически вычисляемых ролей.
    # Поддерживаем auto_observers и head актуальными в соответствии с иерархией компании.
    # head замораживаем при закрытия вакансии.
    department_user = models.ForeignKey(
        to='staff.DepartmentUser',
        blank=True,
        null=True,
        on_delete=_vacancy_membership_delete_mode,
    )

    def __str__(self):
        return '{}, {}, {}'.format(self.vacancy, self.member, self.role)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=['vacancy', 'role'],
                name='vacancies_v_vacancy_a86f7b_partial',
                condition=models.Q(role__in=(
                    choices.VACANCY_ROLES.head,
                    choices.VACANCY_ROLES.hiring_manager,
                )),
            ),
            models.UniqueConstraint(
                fields=['vacancy', 'member', 'role'],
                name='vacancies_v_vacancy_50937c_partial',
                condition=models.Q(department_user__isnull=True),
            ),
            models.UniqueConstraint(
                fields=['vacancy', 'member', 'role', 'department_user'],
                name='vacancies_v_vacancy_70a6bd_partial',
                condition=models.Q(department_user__isnull=False),
            )
        ]
        default_manager_name = 'unsafe'


class VacancyGroup(TimeStampedModel):
    """
    Группа вакансий
    """
    unsafe = models.Manager()
    objects = VacancyGroupPermManager()

    name = models.CharField(max_length=255)
    description = models.TextField(blank=True)

    vacancies = models.ManyToManyField(Vacancy, related_name='groups')
    recruiters = models.ManyToManyField(
        to=settings.AUTH_USER_MODEL,
        through='vacancies.VacancyGroupMembership',
    )

    is_active = models.BooleanField(default=True)
    created_by = models.ForeignKey(
        to=settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
        related_name='created_vacancy_groups',
        null=True,
    )

    def __str__(self):
        return self.name

    class Meta:
        default_manager_name = 'unsafe'


# TODO: вынес это в отдельную модель, т.к. позже
# может появиться связь и с другими пользователями
# с другими ролями
class VacancyGroupMembership(TimeStampedModel):

    unsafe = models.Manager()
    objects = VacancyGroupPermManager()

    vacancy_group = models.ForeignKey(
        to=VacancyGroup,
        on_delete=models.CASCADE,
        related_name='memberships',
    )
    member = models.ForeignKey(
        to=settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='vacancy_group_memberships',
    )

    def __str__(self):
        return '{}, {}'.format(self.vacancy_group, self.member)

    class Meta:
        default_manager_name = 'unsafe'


class VacancySkill(models.Model):

    vacancy = models.ForeignKey(Vacancy, on_delete=models.PROTECT, related_name='vacancy_skills')
    skill = models.ForeignKey(Skill, on_delete=models.PROTECT, related_name='vacancy_skills')
    is_required = models.BooleanField(default=True)

    def __str__(self):
        return '{}, {}'.format(self.vacancy, self.skill)

    class Meta:
        unique_together = ('vacancy', 'skill')


class VacancyCity(models.Model):

    vacancy = models.ForeignKey(Vacancy, on_delete=models.PROTECT, related_name='vacancy_cities')
    city = models.ForeignKey(City, on_delete=models.PROTECT, related_name='vacancy_cities')

    def __str__(self):
        return '{}, {}'.format(self.vacancy, self.city)


class VacancyOffice(models.Model):

    vacancy = models.ForeignKey(Vacancy, on_delete=models.PROTECT, related_name='vacancy_offices')
    office = models.ForeignKey(
        to=Office,
        on_delete=models.PROTECT,
        related_name='vacancy_offices',
    )

    def __str__(self):
        return f'{self.vacancy}, {self.office}'

    class Meta:
        unique_together = ('vacancy', 'office')


class VacancyLocation(models.Model):

    vacancy = models.ForeignKey(Vacancy, on_delete=models.PROTECT, related_name='vacancy_locations')
    location = models.ForeignKey(
        to=Location,
        on_delete=models.PROTECT,
        related_name='vacancy_locations',
    )

    def __str__(self):
        return f'{self.vacancy}, {self.location}'

    class Meta:
        unique_together = ('vacancy', 'location')


class VacancyHistory(models.Model):
    """
    Модель, которая хранит историю изменения статусов и БП вакансии.
    Нужна для того, чтобы периодически делать пуши в OEBS с изменениями.
    actionlog не очень надежный для этой задачи.
    """
    vacancy = models.ForeignKey(Vacancy, on_delete=models.PROTECT, related_name='vacancy_history')
    status = models.CharField(max_length=32, choices=choices.VACANCY_STATUSES)
    resolution = models.CharField(max_length=32, choices=choices.VACANCY_RESOLUTIONS, blank=True)
    budget_position_id = models.IntegerField(null=True, blank=True)
    changed_at = AutoCreatedField()

    # https://st.yandex-team.ru/FEMIDA-4182
    # Это поле, которому вообще не место в этой модели.
    # Но поскольку эту таблицу мы используем для пушей в OEBS, а в OEBS очень нужен ФИО
    # кандидата из соответствующего офера, то проще добавить это поле здесь.
    full_name = models.CharField(max_length=255, null=True, blank=True)

    def __str__(self):
        return '{}, {}, {}'.format(self.vacancy, self.status, self.changed_at)

    @staticmethod
    def _get_full_name(vacancy):
        offer_statuses = (
            choices.VACANCY_STATUSES.offer_processing,
            choices.VACANCY_STATUSES.offer_accepted,
        )
        if vacancy.status in offer_statuses:
            # Офферы в рамках вакансии обрабатываются последовательно,
            # значит мы можем получить соответствующий оффер, отсортировав по дате редактирования.
            offer = vacancy.offers.order_by('-modified').first()
            if offer is not None:
                return offer.full_name

    @classmethod
    def create_from_vacancy(cls, vacancy, **kwargs):
        return cls.objects.create(
            vacancy=vacancy,
            status=kwargs.get('status', vacancy.status),
            budget_position_id=kwargs.get('budget_position_id', vacancy.budget_position_id),
            resolution=vacancy.resolution,
            full_name=cls._get_full_name(vacancy),
        )


class VacancyWorkMode(models.Model):
    vacancy = models.ForeignKey(
        to=Vacancy,
        on_delete=models.PROTECT,
        related_name='vacancy_work_mode'
    )
    work_mode = models.ForeignKey(
        to=WorkMode,
        on_delete=models.PROTECT,
        related_name='vacancy_work_mode'
    )

    def __str__(self):
        return f'{self.vacancy}, {self.work_mode}'


class SubmissionForm(TimeStampedModel):
    """
    Модель, связывающая форму Конструктора Форм с множеством вакансий, на которые она ведет.

    Отклик кандидатов на внешние вакансии происходит через формы КФ,
    опубликованные на Сайте Вакансий.
    После заполнения формы кандидатом, КФ дергает Фемиду (handle_forms_constructor_request)
    с полученными от него данными и id формы.
    SubmissionForm нужна, чтобы связать эту форму с множеством вакансий,
    на которых через нее идет найм.
    SubmissionForm создается ответственным в админке Фемиды для каждой формы Сайта Вакансий.
    """
    title = models.CharField(max_length=255)
    url = models.CharField(max_length=255)
    vacancies = models.ManyToManyField(Vacancy, related_name='submission_forms', blank=True)
    forms_constructor_id = models.IntegerField(blank=True, null=True)
    is_internship = models.BooleanField(default=False)

    def __str__(self):
        return '{} ({})'.format(self.title, self.id)


class PublicationSubscription(TimeStampedModel):

    created_by = models.ForeignKey(
        to=settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
        related_name='publication_subscriptions',
    )
    shown_vacancies = models.ManyToManyField(
        to=Vacancy,
        blank=True,
        related_name='+',
    )
    # Хэш по полям пользовательского фильтра
    sha1 = models.CharField(max_length=40, db_index=True)

    # Поля пользовательского фильтра по публикациям
    text = models.TextField(
        default='',
        blank=True,
    )
    external_url = models.CharField(
        max_length=255,
        blank=True,
    )
    department = models.ForeignKey(
        to='staff.Department',
        on_delete=models.CASCADE,
        related_name='+',
        blank=True,
        null=True,
    )
    abc_services = models.ManyToManyField(
        to='staff.Service',
        blank=True,
        related_name='+',
    )
    professions = models.ManyToManyField(
        to=Profession,
        blank=True,
        related_name='+',
    )
    pro_level_min = models.SmallIntegerField(
        choices=choices.VACANCY_PRO_LEVELS,
        null=True,
        blank=True,
    )
    pro_level_max = models.SmallIntegerField(
        choices=choices.VACANCY_PRO_LEVELS,
        null=True,
        blank=True,
    )
    skills = models.ManyToManyField(
        to=Skill,
        blank=True,
        related_name='+',
    )
    cities = models.ManyToManyField(
        to='core.City',
        blank=True,
        related_name='+',
    )
    only_active = models.BooleanField(
        default=True,
    )

    def __str__(self):
        return 'PublicationSubscription {} ({})'.format(
            self.id,
            self.created_by.username,
        )
