import sform
import waffle

from django.conf import settings
from django.core.exceptions import ValidationError
from django.db.models import Value
from django.db.models.expressions import Subquery, OuterRef
from django.db.models.functions import Coalesce
from django.utils.functional import cached_property


from intranet.femida.src.api.core.forms import (
    PositionSuggestField,
    UserSuggestField,
    PositiveDecimalField,
    PositiveIntegerField,
    ProfessionChoiceField,
    UserMultipleSuggestField,
    AttachmentMultipleSuggestField,
    ServiceMultipleSuggestField,
    DepartmentSuggestField,
    ConditionallyRequiredFieldsMixin,
    EMPTY_LABEL,
    BooleanField,
    BaseMetaForm,
    ChoiceDataMixin,
)
from intranet.femida.src.api.core.validators import validate_phone, validate_birthday
from intranet.femida.src.api.offers.base_forms import (
    OFFER_DATE_INPUT_FORMATS,
    UsernameField,
    InternalFormUsernameField,
    NameValidationFormMixin,
    BankDetailsFormMixin,
)
from intranet.femida.src.api.offers.serializers import RelocationPackageSerializer
from intranet.femida.src.attachments.models import Attachment
from intranet.femida.src.core.models import Currency, LanguageTag
from intranet.femida.src.offers import choices
from intranet.femida.src.offers.helpers import (
    is_hiring_intern_inside,
    is_eds_needed,
)
from intranet.femida.src.offers.login.validators import LoginValidator
from intranet.femida.src.offers.models import Link, Offer, OfferAttachment, RelocationPackage
from intranet.femida.src.offers.startrek.choices import REJECTION_REASONS
from intranet.femida.src.offers.yang.helpers import get_yang_unprocessed_doc_fields
from intranet.femida.src.professions.models import ProfessionalSphere
from intranet.femida.src.staff.choices import GEOGRAPHY_KINDS
from intranet.femida.src.staff.helpers import get_department_chief
from intranet.femida.src.staff.models import Organization, Office, Geography
from intranet.femida.src.utils.switches import is_rotation_enabled
from intranet.femida.src.utils.translation import get_language

CONDITION_IS_EXTERNAL = ('employee_type', choices.EXTERNAL_EMPLOYEE_TYPES)

CONDITION_IS_NOT_ROTATION = ('employee_type', {
    choices.EMPLOYEE_TYPES.intern,
    choices.EMPLOYEE_TYPES.new,
    choices.EMPLOYEE_TYPES.former,
    choices.EMPLOYEE_TYPES.current,
})


# TODO: вынести это в sform
class NotNullNumberFieldMixin:
    """
    Миксин, который вместо пустых значений
    для чисел возвращает 0
    """

    def clean(self, *args, **kwargs):
        value = super().clean(*args, **kwargs)
        return value or 0


class MoneyField(NotNullNumberFieldMixin, PositiveDecimalField):

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('max_digits', 10)
        kwargs.setdefault('decimal_places', 2)
        super().__init__(*args, **kwargs)


class ChoiceFieldMixin:
    """
    Миксин для селектов. Добавляет следующий функционал:
    - полю по умолчанию проставляется empty_label;
    - если поле обязательное и уже содержит какое-то значение,
      empty_label удаляется;
    """

    def __init__(self, *args, **kwargs):
        kwargs['empty_label'] = kwargs.get('empty_label', EMPTY_LABEL)
        super().__init__(*args, **kwargs)

    def structure_as_dict(self, *args, **kwargs):
        field_dict = super().structure_as_dict(*args, **kwargs)
        initial_data = kwargs['base_initial']

        # Для FieldsetField
        prefix = kwargs.get('prefix')
        if prefix:
            initial_data = initial_data.get(prefix, {})

        initial = initial_data.get(kwargs['name'])
        has_initial = (
            initial is not None and initial != ''
            or self.default is not None and self.default != ''
        )

        if (kwargs['state'] == sform.REQUIRED
                and has_initial
                and self.empty_label is not None):
            del field_dict['choices'][0]
            self.empty_label = None

        return field_dict


class ChoiceField(ChoiceFieldMixin, sform.ChoiceField):

    pass


class ModelChoiceField(ChoiceFieldMixin, sform.ModelChoiceField):

    pass


class RelocationPackageChoiceField(ChoiceDataMixin, sform.ModelChoiceField):
    type_name = 'choice'
    choice_serializer_class = RelocationPackageSerializer


class UnfilledBooleanField(sform.NullBooleanField):
    """
    Костыльное поле. Подстроились под фронт,
    чтобы не делать доработки ещё и там.
    Нужно для того, чтобы на фронте отобразить BooleanField
    как изначально незаполненный с 2 вариантами "Да/Нет"
    """
    def structure_as_dict(self, *args, **kwargs):
        structure = super().structure_as_dict(*args, **kwargs)
        structure['type'] = 'boolean'
        return structure


class BPForm(BaseMetaForm):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.bp_data = self.context.get('bp_data', {})

    def get_bp_data(self):
        return {k: v for k, v in self.bp_data.items() if k in self.fields}

    def meta_as_dict(self):
        return {
            'bp_data': self.get_bp_data(),
        }


class OfferBaseForm(ConditionallyRequiredFieldsMixin, BPForm):
    """
    Базовая форма оффера.
    То, что доступно для редактирования всегда.
    """
    full_name = sform.CharField(
        max_length=255,
        state=sform.REQUIRED,
    )
    employee_type = ChoiceField(
        choices=choices.EMPLOYEE_TYPES,
        state=sform.REQUIRED,
    )
    username = InternalFormUsernameField()

    position = PositionSuggestField()
    staff_position_name = sform.CharField(max_length=255)
    join_at = sform.DateField(state=sform.REQUIRED)
    is_main_work_place = sform.BooleanField()
    probation_period_type = ChoiceField(choices.PROBATION_PERIOD_TYPES)

    # Benefits
    sick_leave_supplement = sform.BooleanField()
    housing_program = sform.BooleanField()
    cellular_compensation = sform.BooleanField(default=False)
    internet_compensation_amount = MoneyField()

    form_type = ChoiceField(choices=choices.FORM_TYPES)

    CONDITIONALLY_REQUIRED = {
        'form_type': CONDITION_IS_EXTERNAL,
        'probation_period_type': CONDITION_IS_EXTERNAL,
        'position': CONDITION_IS_NOT_ROTATION,
        'staff_position_name': CONDITION_IS_NOT_ROTATION,
    }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not is_rotation_enabled():
            self.fields['employee_type'].choices = choices.EXTERNAL_EMPLOYEE_TYPES

    def _get_employee_type(self):
        return self.cleaned_data.get('employee_type') or choices.EMPLOYEE_TYPES.new

    def get_field_state(self, name):
        if name == 'username' and self._get_employee_type() != choices.EMPLOYEE_TYPES.new:
            return sform.REQUIRED
        # Не даём менять дату выхода для автонайма, чтобы не возникало рассинхронов с БП
        if name == 'join_at' and self.initial['is_autohire']:
            return sform.READONLY
        return super().get_field_state(name)

    def clean_username(self, username):
        if username:
            LoginValidator(
                employee_type=self._get_employee_type(),
                exclude_offer_ids=self.initial['id'],
            ).validate(username)
        return username

    def meta_as_dict(self):
        meta = super().meta_as_dict()
        extra_meta = {
            'headcount': self.bp_data.get('headcount'),
            'has_old_format_rsu': bool(self.initial['rsu']),
        }
        if not waffle.switch_is_active('disable_food_compensation_amount'):
            extra_meta['food_compensation_amount'] = self.bp_data.get('food_compensation_amount')
        return dict(meta, **extra_meta)

    def clean(self):
        cd = super().clean()
        if self._get_employee_type() in choices.INTERNAL_EMPLOYEE_TYPES:
            # Игнорируем поля неактуальные для внутреннего оффера
            for field in Offer.EXTERNAL_ONLY_FIELDS:
                cd.pop(field, None)
        probation_period_type = cd.pop('probation_period_type', choices.PROBATION_PERIOD_TYPES.no)
        if probation_period_type:
            cd['probation_period'], cd['probation_period_unit'] = (
                choices.PROBATION_PERIOD_TYPE_TO_UNITS[probation_period_type]
            )
        return cd


class InternalOfferBaseForm(OfferBaseForm):
    """
    Базовая форма внутреннего оффера.
    То, что доступно для редактирования всегда.
    """
    def get_field_state(self, name):
        if name in ('employee_type', 'username') and self.context['offer'].is_internal:
            return sform.READONLY
        return super().get_field_state(name)


class ExternalOfferBaseForm(OfferBaseForm):
    """
    Базовая форма внешнего оффера.
    То, что доступно для редактирования всегда.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # Делаем это в __init__, чтобы не изменился порядок полей
        self.fields['employee_type'].choices = choices.EXTERNAL_EMPLOYEE_TYPES


class OfferForm(OfferBaseForm):
    """
    Форма заполнения данных оффера.
    То, что можно редактировать до отправки на согласование.
    """
    org = ModelChoiceField(
        queryset=Organization.objects.alive(),
        label_extractor=str,
    )
    department = DepartmentSuggestField(state=sform.REQUIRED)
    geography = sform.ModelChoiceField(
        queryset=Geography.objects.alive().exclude_roots(),
        label_extractor=lambda x: x.name,
        state=sform.REQUIRED,
    )
    grade = PositiveIntegerField(max_value=100, state=sform.REQUIRED)
    payment_type = ChoiceField(choices.PAYMENT_TYPES)
    salary = PositiveDecimalField(
        max_digits=10,
        decimal_places=2,
    )
    hourly_rate = PositiveDecimalField(
        max_digits=10,
        decimal_places=2,
    )

    employment_type = ChoiceField(choices.EMPLOYMENT_TYPES)
    work_hours_weekly = PositiveIntegerField(max_value=168)

    work_place = ChoiceField(choices.WORK_PLACES)
    office = ModelChoiceField(
        queryset=(
            Office.objects
            .alive()
            .exclude_groups()
            .order_by('name_ru')
        ),
        label_extractor=lambda x: x.name,
    )
    homeworker_location = sform.CharField(max_length=255)

    contract_type = ChoiceField(choices.OFFER_FORM_CONTRACT_TYPES)
    contract_term = PositiveIntegerField(max_value=1200)
    contract_term_date = sform.DateField()

    # Benefits
    vmi = UnfilledBooleanField()
    rsu_cost = MoneyField()
    signup_bonus = MoneyField()
    signup_2year_bonus = MoneyField()
    bonus_type = ChoiceField(choices.BONUS_TYPES)
    bonus = MoneyField()
    bonus_2year = MoneyField()
    allowance = MoneyField()
    need_relocation = sform.BooleanField(default=False)
    relocation_package = ChoiceField(choices.RELOCATION_PACKAGE)
    relocation_package_data = RelocationPackageChoiceField(
        queryset=RelocationPackage.objects.all(),
        state=sform.READONLY,
        to_field_name='type',
    )

    payment_currency = ModelChoiceField(
        empty_label=EMPTY_LABEL,
        queryset=Currency.active.all(),
        label_extractor=lambda x: x.code,
    )

    professional_sphere = ModelChoiceField(
        queryset=ProfessionalSphere.active.all(),
        label_extractor=lambda x: x.localized_name,
    )
    profession = ProfessionChoiceField(
        empty_label=EMPTY_LABEL,
        state=sform.REQUIRED,
    )

    CONDITIONALLY_REQUIRED = dict(OfferBaseForm.CONDITIONALLY_REQUIRED, **{
        'org': CONDITION_IS_EXTERNAL,
        'work_place': CONDITION_IS_EXTERNAL,
        'homeworker_location': ('work_place', choices.WORK_PLACES.home),
        'office': ('work_place', choices.WORK_PLACES.office),
        'payment_type': CONDITION_IS_NOT_ROTATION,
        'salary': CONDITION_IS_NOT_ROTATION,
        'employment_type': CONDITION_IS_NOT_ROTATION,
        'work_hours_weekly': ('employment_type', choices.EMPLOYMENT_TYPES.partial),
        'contract_type': CONDITION_IS_NOT_ROTATION,
        'contract_term': ('contract_type', choices.CONTRACT_TYPES.fixed_term),
        'contract_term_date': ('contract_type', choices.CONTRACT_TYPES.fixed_term_date),
    })

    # TODO: удалить после релиза STAFF-13983 и когда не останется офферов без профессий
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        if not waffle.switch_is_active('enable_offer_profession_in_main_form'):
            self.fields.pop('profession')
            self.fields.pop('professional_sphere')

        expected_geography_kind = (
            GEOGRAPHY_KINDS.international if self.context['geography_international']
            else GEOGRAPHY_KINDS.rus
        )
        self.fields['geography'].queryset = self.fields['geography'].queryset.filter(
            kind=expected_geography_kind,
        ).order_by_tree()

    def get_field_state(self, name):
        if name == 'payment_currency':
            # Для внешних офферов и переводов стажеров валюта обязательна
            if self._get_employee_type() != choices.EMPLOYEE_TYPES.rotation:
                return sform.REQUIRED

            # Если заполнены поля про деньги, которые зависят от валюты,
            # валюта становится обязательной
            currency_depended_fields = (
                'salary',
                'hourly_rate',
                'allowance',
                'internet_compensation_amount',
            )
            if any(self.cleaned_data.get(f) for f in currency_depended_fields):
                return sform.REQUIRED
        elif name == 'hourly_rate':
            is_required = (
                self.cleaned_data.get('payment_type') == choices.PAYMENT_TYPES.hourly
                and self._get_employee_type() != choices.EMPLOYEE_TYPES.rotation
            )
            if is_required:
                return sform.REQUIRED
        elif name == 'vmi':
            if waffle.switch_is_active('enable_scheme_requests'):
                return sform.READONLY
            if self.cleaned_data.get('employee_type') == choices.EMPLOYEE_TYPES.rotation:
                return sform.NORMAL
            return sform.REQUIRED

        return super().get_field_state(name)

    def get_professional_sphere(self, initial_data):
        # Нужно только в форме. В БД хранить ни к чему
        profession_id = initial_data.get('profession')
        if not isinstance(profession_id, int):
            return None

        return self.fields['profession'].prof_sphere_map.get(profession_id)

    def clean_department(self, department):
        if not department:
            return department
        if get_department_chief(department) is None:
            raise ValidationError('department_with_no_chief')
        return department

    def clean(self):
        cleaned_data = super().clean()

        work_place = cleaned_data.get('work_place')
        if work_place == choices.WORK_PLACES.home:
            cleaned_data['office'] = Office.objects.get(pk=settings.OFFICE_HOMEWORKER_ID)

        employment_type = cleaned_data.get('employment_type')
        if employment_type == choices.EMPLOYMENT_TYPES.full:
            cleaned_data['work_hours_weekly'] = 40

        # Это поле нужно только для фронта
        cleaned_data.pop('professional_sphere', None)

        return cleaned_data


class OfferApproveBaseForm(BPForm):

    abc_services = ServiceMultipleSuggestField(state=sform.REQUIRED)
    professional_sphere = ModelChoiceField(
        queryset=ProfessionalSphere.active.all(),
        label_extractor=lambda x: x.localized_name,
    )
    profession = ProfessionChoiceField(
        empty_label=EMPTY_LABEL,
        state=sform.REQUIRED,
    )
    professional_level = ChoiceField(
        choices.PROFESSIONAL_LEVELS,
        state=sform.REQUIRED,
    )
    programming_language = ChoiceField(choices=choices.ST_PROGRAMMING_LANGUAGES)

    # TODO: удалить после релиза STAFF-13983 и когда не останется офферов без профессий
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.offer = self.context['offer']
        if waffle.switch_is_active('enable_offer_profession_in_main_form'):
            if self.offer.profession:
                self.fields.pop('profession')
                self.fields.pop('professional_sphere')

    def get_professional_sphere(self, initial_data):
        # Нужно только в форме. В БД хранить ни к чему
        profession_id = initial_data.get('profession')
        if not isinstance(profession_id, int):
            return None

        return self.fields['profession'].prof_sphere_map.get(profession_id)

    def clean(self):
        cleaned_data = super().clean()
        # Это поле нужно только для фронта
        cleaned_data.pop('professional_sphere', None)
        return cleaned_data


class InternalOfferApproveForm(OfferApproveBaseForm):
    """
    Форма согласования внутреннего оффера
    """


class ExternalOfferApproveForm(OfferApproveBaseForm):
    """
    Форма согласования внешнего оффера
    """
    other_payments = sform.CharField(max_length=255)
    salary_expectations = MoneyField()
    salary_expectations_currency = ModelChoiceField(
        queryset=Currency.active.all(),
        label_extractor=lambda x: x.code,
        state=sform.REQUIRED,
    )
    current_company = sform.CharField(max_length=255)
    source = ChoiceField(choices.SOURCES, state=sform.REQUIRED)
    source_description = sform.CharField(max_length=255)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['salary_expectations_currency'].default = (
            Currency.objects.values_list('id', flat=True).filter(code='RUB').first()
        )


class CheckLoginForm(sform.SForm):
    """
    SForm проверки логина
    """
    employee_type = ChoiceField(
        choices=choices.EMPLOYEE_TYPES,
        state=sform.REQUIRED,
    )
    username = InternalFormUsernameField(state=sform.REQUIRED)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.offer = self.context.get('offer')
        assert self.offer is not None

    def clean_username(self, username):
        if username:
            LoginValidator(
                employee_type=self.cleaned_data.get('employee_type'),
                newhire_id=self.offer.newhire_id,
                exclude_offer_ids=self.offer.id,
            ).validate(username)
        return username


class CheckLoginExternalForm(sform.SForm):
    """
    SForm проверки логина (для внешней анкеты)
    """
    username = UsernameField(state=sform.REQUIRED)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.offer = self.context.get('offer')
        assert self.offer is not None

    def clean_username(self, username):
        if username:
            LoginValidator(
                employee_type=self.offer.employee_type,
                newhire_id=self.offer.newhire_id,
                exclude_offer_ids=self.offer.id,
            ).validate(username)
        return username


class OfferAttachmentMultipleSuggestField(AttachmentMultipleSuggestField):

    def __init__(self, *args, **kwargs):
        self.max_files = kwargs.pop('max_files', None)
        queryset = self._attachment_queryset()
        super(AttachmentMultipleSuggestField, self).__init__(
            queryset=queryset,
            to_field_name='id',
            label_fields={
                'caption': ['name'],
                'extra_fields': ['type'],
            },
            **kwargs
        )

    def _attachment_queryset(self):
        offer_attachments_types_subq = OfferAttachment.objects.filter(
            attachment_id=OuterRef('id')
        ).values('type')
        return Attachment.objects.all().annotate(
            type=Coalesce(Subquery(offer_attachments_types_subq), Value('other'))
        )


class OfferSendForm(sform.SForm):
    """
    Форма отправки оффера кандидату
    """
    receiver = sform.EmailField(state=sform.REQUIRED)
    bcc = UserMultipleSuggestField()
    subject = sform.CharField(
        max_length=255,
        state=sform.REQUIRED,
        default='Яндекс. Предложение о работе',
    )
    message = sform.CharField(state=sform.REQUIRED)
    offer_text = sform.CharField()
    attachments = OfferAttachmentMultipleSuggestField()

    def clean_offer_text(self, offer_text):
        if not offer_text:
            return offer_text
        link = Link.objects.filter(offer=self.context['offer']).first()
        if link and str(link) not in offer_text:
            raise ValidationError(
                message='Link must be in message',
                code='link_required',
            )
        return offer_text


class OfferRejectForm(sform.SForm):
    """
    Форма отказа от оффера
    """
    recruiter = UserSuggestField(state=sform.REQUIRED)
    rejection_reason = ChoiceField(REJECTION_REASONS, state=sform.REQUIRED)
    salary_expectations = MoneyField()
    salary_expectations_currency = ModelChoiceField(
        queryset=Currency.active.all(),
        label_extractor=lambda x: x.code,
    )
    competing_offer_conditions = sform.CharField()
    competing_company = sform.CharField(max_length=255)
    comment = sform.CharField()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['salary_expectations_currency'].default = (
            Currency.objects.values_list('id', flat=True).filter(code='RUB').first()
        )


class OfferDeleteForm(sform.SForm):
    """
    Форма удаления оффера
    """
    comment = sform.CharField()

    def get_field_state(self, name):
        if name == 'comment' and waffle.switch_is_active('require_comment_on_offer_delete'):
            return sform.REQUIRED
        return super().get_field_state(name)


class CitizenshipChoiceField(ChoiceField):
    """
    По просьбе с фронтенда подвозим человекочитаемые отсортированные названия гражданств
    в соответствии с языком интерфейса.
    """
    def __init__(self, *args, **kwargs):
        self._choices_i18n = {
            'EN': choices.CITIZENSHIP_EN,
            'RU': choices.CITIZENSHIP_RU,
        }
        super().__init__(choices=self._choices_i18n['EN'], *args, **kwargs)

    def structure_as_dict(self, *args, **kwargs):
        self.choices = self._choices_i18n.get(get_language().upper(), self.choices)
        return super().structure_as_dict(*args, **kwargs)


class OfferAcceptBaseForm(NameValidationFormMixin, BankDetailsFormMixin, BPForm):
    """
    Внешняя форма анкеты кандидата
    """
    last_name = sform.CharField(max_length=100, state=sform.REQUIRED)
    first_name = sform.CharField(max_length=50, state=sform.REQUIRED)
    gender = ChoiceField(choices=choices.GENDER, state=sform.REQUIRED)
    birthday = sform.DateField(
        state=sform.REQUIRED,
        input_formats=OFFER_DATE_INPUT_FORMATS,
        validators=[validate_birthday],
    )
    citizenship = CitizenshipChoiceField(state=sform.REQUIRED)
    is_full_time_education = sform.NullBooleanField(state=sform.REQUIRED)
    has_education_confirmation = sform.NullBooleanField()
    residence_address = sform.CharField(max_length=1024, state=sform.REQUIRED)
    phone = sform.CharField(
        max_length=50,
        state=sform.REQUIRED,
        validators=[validate_phone],
    )
    os = ChoiceField(choices=choices.AVAILABLE_OPERATING_SYSTEMS, state=sform.REQUIRED)
    home_email = sform.EmailField(max_length=50, state=sform.REQUIRED)
    photo = AttachmentMultipleSuggestField(max_files=1, state=sform.REQUIRED)
    username = UsernameField(state=sform.REQUIRED)
    join_at = sform.DateField(state=sform.REQUIRED, input_formats=OFFER_DATE_INPUT_FORMATS)
    is_agree = BooleanField(state=sform.REQUIRED)
    nda_accepted = BooleanField(state=sform.REQUIRED)
    is_eds_needed = BooleanField()

    @cached_property
    def offer(self):
        offer = self.context.get('offer')
        assert offer is not None
        return offer

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

        # Файлы можно выбирать только из загруженных именно к этому офферу
        attachments_qs = self.offer.attachments.all()
        for field_name in self.fields:
            field = self.fields[field_name]
            if isinstance(field, AttachmentMultipleSuggestField):
                field.queryset = attachments_qs

        if not is_hiring_intern_inside(self.offer):
            self.fields.pop('is_full_time_education')
            self.fields.pop('has_education_confirmation')

        if not is_eds_needed(self.offer):
            self.fields.pop('is_eds_needed')

        if self.context.get('is_with_hireorder_autofill'):
            self.fields.pop('is_agree')
            self.fields.pop('nda_accepted')

    def is_bank_details_needed(self):
        return self.offer.is_bank_details_needed

    def get_field_state(self, name):
        if name == 'username' and self.offer.employee_type != choices.EMPLOYEE_TYPES.new:
            return sform.READONLY
        if name == 'has_education_confirmation':
            if self.cleaned_data.get('is_full_time_education'):
                return sform.REQUIRED
        return super().get_field_state(name)

    def clean_username(self, username):
        if username:
            LoginValidator(
                employee_type=self.offer.employee_type,
                newhire_id=self.offer.newhire_id,
                exclude_offer_ids=self.offer.id,
            ).validate(username)
        return username

    def clean(self):
        cleaned_data = super().clean()
        data = {}
        root_fields = (
            'username',
            'join_at',
            'comment',
            'is_agree',
            'bank_details',
            'is_eds_needed',
        )
        for field in root_fields:
            if field in cleaned_data:
                data[field] = cleaned_data.pop(field)

        name_fields = ('last_name', 'first_name', 'middle_name')
        full_name_parts = (cleaned_data.get(i, '') for i in name_fields)
        data['full_name'] = ' '.join(i for i in full_name_parts if i)

        data['profile'] = cleaned_data
        return data

    def meta_as_dict(self):
        meta = super().meta_as_dict()
        extra_meta = {
            'organization_name': self.bp_data.get('org', ''),
            'form_type': self.offer.form_type,
            'need_relocation': self.offer.need_relocation,
            'is_homeworker': self.offer.work_place == choices.WORK_PLACES.home,
            'is_yandex_department': self.offer.department.is_in_tree(settings.YANDEX_DEPARTMENT_ID),
        }
        return dict(meta, **extra_meta)


class OfferAcceptRussianForm(OfferAcceptBaseForm):

    middle_name = sform.CharField(max_length=50)
    last_name_en = sform.CharField(max_length=100, state=sform.REQUIRED)
    first_name_en = sform.CharField(max_length=50, state=sform.REQUIRED)
    employment_book = ChoiceField(choices.EMPLOYMENT_BOOK_OPTIONS, state=sform.REQUIRED)
    passport_pages = AttachmentMultipleSuggestField(max_files=settings.OFFER_MAX_ATTACHMENTS_COUNT)
    snils = AttachmentMultipleSuggestField(max_files=1)
    comment = sform.CharField()

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        is_employment_book_needed = (
            self.offer.is_main_work_place
            and self.offer.org.is_russian
        )
        if not is_employment_book_needed:
            self.fields.pop('employment_book')


def _language_tag_label(tag):
    if tag.native == tag.name:
        return f"{tag.native}"
    else:
        return f"{tag.native} ({tag.name})"


class OfferAcceptInternationalForm(OfferAcceptBaseForm):

    documents = AttachmentMultipleSuggestField(max_files=settings.OFFER_MAX_ATTACHMENTS_COUNT)
    preferred_first_and_last_name = sform.CharField(max_length=255, state=sform.REQUIRED)
    main_language = sform.ModelChoiceField(
        queryset=LanguageTag.objects.filter(tag__in=['ru', 'en']),
        label_extractor=_language_tag_label,
        state=sform.REQUIRED,
    )
    spoken_languages = sform.ModelMultipleChoiceField(
        queryset=LanguageTag.objects.all(),
        label_extractor=_language_tag_label,
        state=sform.NORMAL,
    )

    # Для международных анкет имя должно быть на латинице
    clean_first_name = NameValidationFormMixin._clean_name_en
    clean_last_name = NameValidationFormMixin._clean_name_en

    def clean(self):
        cleaned_data = super().clean()
        cleaned_data['profile']['last_name_en'] = cleaned_data['profile'].get('last_name', '')
        cleaned_data['profile']['first_name_en'] = cleaned_data['profile'].get('first_name', '')
        return cleaned_data


class OfferShortAcceptForm(sform.SForm):
    """
    Сокращенная форма внешней анкеты кандидата для запроса недостающих документов
    """
    citizenship = CitizenshipChoiceField(state=sform.READONLY)
    passport_pages = AttachmentMultipleSuggestField(max_files=settings.OFFER_MAX_ATTACHMENTS_COUNT)
    snils = AttachmentMultipleSuggestField(max_files=1)
    residence_address = sform.CharField(max_length=1024, state=sform.REQUIRED)

    def get_citizenship(self, initial):
        return self.offer.profile.citizenship

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.offer = self.context['offer']

        # read-only поля должны оставаться в форме всегда, остальные – по необходимости
        required_fields = {f for f in self.fields if self.fields[f].state == sform.READONLY}
        required_fields.update(get_yang_unprocessed_doc_fields(self.offer))

        for field in list(self.fields):
            if field not in required_fields:
                self.fields.pop(field)


class OfferUpdateJoinAtForm(sform.SForm):
    """
    Форма изменения даты выхода
    """
    join_at = sform.DateField(state=sform.REQUIRED)


class OfferUpdateUsernameForm(sform.SForm):
    """
    Форма изменения логина.
    Отдаётся только для внешних офферов.
    """
    employee_type = ChoiceField(
        choices=choices.EXTERNAL_EMPLOYEE_TYPES,
        state=sform.REQUIRED,
    )
    username = UsernameField(state=sform.REQUIRED)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.offer = self.context.get('offer')
        assert self.offer is not None

    def _get_employee_type(self):
        return self.cleaned_data.get('employee_type') or choices.EMPLOYEE_TYPES.new

    def clean_username(self, username):
        if username:
            LoginValidator(
                employee_type=self._get_employee_type(),
                newhire_id=self.offer.newhire_id,
                exclude_offer_ids=self.offer.id,
            ).validate(username)
        return username


class OfferUpdateDepartmentForm(BaseMetaForm):
    """
    Форма изменения подразделения
    """
    department = DepartmentSuggestField(state=sform.REQUIRED)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        offer = self.context.get('offer')
        assert offer is not None

        # Можно выбирать подразделение только из той же
        # корневой ветки, что и действующее.
        department = offer.department
        root_id = department.ancestors[0] if department.ancestors else department.id
        self.fields['department'].queryset = self.fields['department'].queryset.in_tree(root_id)

    def get_field_state(self, name):
        if name == 'department' and self.department_edit_is_readonly():
            return sform.READONLY

        return super().get_field_state(name)

    def meta_as_dict(self):
        meta = super().meta_as_dict()
        meta['department_edit_moved_to_proposal'] = self.department_edit_is_readonly()
        return meta

    def department_edit_is_readonly(self):
        return waffle.switch_is_active('disable_department_edit_in_favor_of_proposal')
