from collections import OrderedDict, defaultdict

import pytz
import sform

from django import forms
from django.db import transaction
from django.db.utils import IntegrityError
from django.forms.fields import DecimalField, IntegerField
from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
from django.utils.functional import cached_property
from django.utils.translation import ugettext_lazy as _
from django.contrib.auth import get_user_model
from model_utils import Choices

from intranet.femida.src.api.applications.serializers import ApplicationAsChoiceSerializer
from intranet.femida.src.api.candidates.serializers import CandidateDuplicateAsChoiceSerializer
from intranet.femida.src.interviews.models import Interview
from intranet.femida.src.jobs.public_professions.models import PublicProfession
from intranet.femida.src.staff.models import Service, Department, ValueStream, Geography, Office
from intranet.femida.src.offers.models import Position
from intranet.femida.src.skills.models import Skill
from intranet.femida.src.professions.models import Profession
from intranet.femida.src.publications.helpers import get_publications_lang
from intranet.femida.src.candidates.models import Candidate, Challenge, Consideration
from intranet.femida.src.communications.choices import MESSAGE_TYPES
from intranet.femida.src.communications.models import Message
from intranet.femida.src.vacancies.models import Vacancy
from intranet.femida.src.attachments.models import Attachment
from intranet.femida.src.publications.helpers import count_external_publications_annotation
from intranet.femida.src.core.exceptions import SimpleValidationError
from intranet.femida.src.core.models import City, Currency, Tag, Location, WorkMode
from intranet.femida.src.applications.models import Application
from intranet.femida.src.problems.models import Category, Problem, Preset
from intranet.femida.src.api.core.choice_serializers import (
    CityAsChoiceSerializer,
    InterviewAsChoiceSerializer,
    ProfessionAsChoiceSerializer,
    PublicServiceAsChoiceSerializer,
    SkillAsChoiceSerializer,
    ConsiderationAsChoiceSerializer,
    PublicProfessionAsChoiceSerializer,
)
from intranet.femida.src.services.models import PublicService
from intranet.femida.src.utils.translation import get_name_field, get_localized_name_field
from intranet.femida.src.utils.geosearch import GeosearchAPI, GeosearchError


EMPTY_LABEL = '\u2014'
PG_MAX_INTEGER_VALUE = 2147483647


def default_user_label_extractor(user):
    return f'{user.first_name} {user.last_name}, {user.username}@'


def grid_values_matcher_by_id(old_values, new_values, default):
    """
    values_matcher для GridField, где мы хотим сохранять маппинг по id
    Чтобы сортировка/удаление/добавление не приводило к переписыванию всех
    вложенных сущностей.
    """
    mapping = {i['id']: i for i in old_values}
    for new_value in new_values:
        new_value_id = new_value.get('id')
        old_value = mapping.get(int(new_value_id), default) if new_value_id else None
        yield old_value, new_value


def validate_recruiters(recruiters, *, allow_recruiter_assessor=False):
    errors = []
    for recruiter in recruiters:
        if recruiter.is_recruiter or allow_recruiter_assessor and recruiter.is_recruiter_assessor:
            continue
        errors.append(
            ValidationError(
                message='User is not a recruiter',
                code='user_is_not_a_recruiter',
                params={
                    'invalid_username': recruiter.username,
                },
            )
        )
    if errors:
        raise ValidationError(errors)


class ChoiceDataMixin:
    """
    Подкладывает в choice-поля кроме обычных лейблов ещё дополнительные данные.
    Лучше когда-нибудь унести эту логику в sform в базовые классы choice-полей.
    """
    choice_serializer_class = None

    def __init__(self, *args, **kwargs):
        assert 'label_extractor' not in kwargs
        assert self.choice_serializer_class is not None
        kwargs['label_extractor'] = lambda x: self.choice_serializer_class(x).data
        super().__init__(*args, **kwargs)

    def structure_as_dict(self, *args, **kwargs):
        structure = super().structure_as_dict(*args, **kwargs)
        for choice in structure['choices']:
            if not isinstance(choice.get('label'), dict):
                continue
            label_data = choice.pop('label')
            choice['label'] = label_data.pop('label')
            choice['data'] = label_data
        return structure


# Field в sform автоматически проставляет type, беря название класса филда
# и отрезая "Field". Поэтому переопределение филдов привело к изменению type
class SuggestField(sform.SuggestField):

    def structure_as_dict(self, *args, **kwargs):
        result = super().structure_as_dict(*args, **kwargs)
        result['type'] = 'suggest'
        return result


class MultipleSuggestField(sform.MultipleSuggestField):

    def __init__(self, queryset, **kwargs):
        if 'label_fields' not in kwargs:
            kwargs['label_fields'] = {
                'caption': ['id'],
                'extra_fields': [],
            }
        super().__init__(
            queryset=queryset,
            **kwargs
        )

    def structure_as_dict(self, *args, **kwargs):
        result = super().structure_as_dict(*args, **kwargs)
        result['type'] = 'multiplesuggest'
        return result


class AbstractExternalSuggestField(sform.Field):
    """
    Если нужен suggest по сущностям, которых нет в нашей базе, то можно использовать это поле
    """
    def __init__(self, resource_type, dj_field, *args, **kwargs):
        self.resource_type = resource_type
        self.dj_field = dj_field
        super().__init__(*args, **kwargs)

    def data_to_dict(self, *args, **kwargs):
        raise NotImplementedError


class ExternalSuggestField(AbstractExternalSuggestField):

    def structure_as_dict(self, *args, **kwargs):
        field_dict = super().structure_as_dict(*args, **kwargs)
        field_dict['type'] = 'suggest'
        field_dict['types'] = [self.resource_type]
        return field_dict

    def get_dj_field(self, required=False):
        return self.dj_field


class SuggestCreateField(sform.SuggestField):
    """
    Если передан флаг create_missing, то создает недостающие сущности,
    иначе ведет себя как обычный SuggestField
    """
    def __init__(self, *args, **kwargs):
        self.create_missing = kwargs.pop('create_missing', False)
        super().__init__(*args, **kwargs)

    def clean(self, new_value, *args, **kwargs):
        try:
            with transaction.atomic():
                self.create(new_value)
        except IntegrityError:
            pass
        return super().clean(new_value, *args, **kwargs)

    def create(self, data):
        raise NotImplementedError


class MultipleSuggestCreateField(MultipleSuggestField):
    """
    Если передан флаг create_missing, то создает недостающие сущности,
    иначе ведет себя как обычный MultipleSuggestField
    """
    def __init__(self, *args, **kwargs):
        self.create_missing = kwargs.pop('create_missing', False)
        super().__init__(*args, **kwargs)

    def clean(self, new_value, *args, **kwargs):
        if self.create_missing:
            self.create(new_value)
        return super().clean(new_value, *args, **kwargs)

    def create(self, data):
        raise NotImplementedError


class TagMultipleSuggestField(MultipleSuggestCreateField):

    def create(self, data):
        self.queryset.bulk_create(
            objs=[Tag(name=name) for name in set(data)],
            ignore_conflicts=True,
        )

    def extract_new_value(self, data, name):
        new_value = super().extract_new_value(data, name)
        stripped_tags_gen = (t.strip() for t in new_value)
        return [t for t in stripped_tags_gen if t]

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Tag.objects.all(),
            to_field_name='name',
            label_fields={
                'caption': ['name'],
                'extra_fields': [],
            },
            **kwargs
        )


class LocationHelperForm(sform.SForm):

    geo_id = sform.IntegerField(state=sform.REQUIRED)
    geocoder_uri = sform.CharField()


class LocationMultipleSuggestHelperForm(sform.SForm):

    locations = sform.GridField(sform.FieldsetField(LocationHelperForm))


class LocationMultipleSuggestField(MultipleSuggestCreateField):

    def clean(self, new_value, *args, **kwargs):
        form = LocationMultipleSuggestHelperForm({'locations': new_value})
        if not form.is_valid():
            raise SimpleValidationError('invalid')
        geo_ids = [i['geo_id'] for i in form.cleaned_data['locations']]
        geo_ids_length = len(geo_ids)

        # max location length is set to ten because of GeosearchAPI.
        # It can process by default maximum of 10 locations.
        if geo_ids_length > 10:
            raise ValidationError(code='too_many_locations', message="Too many locations.")

        if geo_ids_length != len(set(geo_ids)):
            raise ValidationError(code='not_unique_locations', message="Locations must be unique.")

        uris = [i['geocoder_uri'] for i in form.cleaned_data['locations'] if i.get('geocoder_uri')]

        if self.create_missing and uris:
            self.create(uris)

        # Note: Вызываем напрямую метод MultipleSuggestField,
        # т.к. нам не нужна логика из MultipleSuggestCreateField
        return MultipleSuggestField.clean(self, geo_ids, *args, **kwargs)

    def create(self, data):
        try:
            locations_data = GeosearchAPI.get_multilang_geo_objects(data)
        except GeosearchError as error:
            raise ValidationError(code='geosearch_error', message=error.message)

        for location_data in locations_data:
            geo_id = location_data.pop('geo_id')
            self.queryset.update_or_create(
                geo_id=geo_id,
                defaults=location_data,
            )

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Location.objects.all(),
            to_field_name='geo_id',
            label_fields={
                'caption': ['name_ru'],
                'extra_fields': [],
            },
            **kwargs
        )

    def get_label_fields(self, label_fields=None):
        return super().get_label_fields(get_name_field())


class ArrayField(forms.Field):

    default_error_messages = {
        'invalid_list': _('Enter a list of values.'),
    }

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

    def to_python(self, value):
        if not value:
            return []
        elif not isinstance(value, (list, tuple)):
            raise ValidationError(self.error_messages['invalid_list'], code='invalid_list')
        result = [self.child.to_python(item) for item in value]
        return result


class ExternalMultipleSuggestField(AbstractExternalSuggestField):

    def get_dj_field(self, required=False):
        return ArrayField(self.dj_field, required=required)

    def structure_as_dict(self, *args, **kwargs):
        field_dict = super().structure_as_dict(*args, **kwargs)
        field_dict['type'] = 'multiplesuggest'
        field_dict['types'] = [self.resource_type]
        field_dict['value'] = []
        return field_dict


class SortFieldMixin:

    dj_field_class = None

    def get_dj_field(self, required=False):
        choices = list(self.choices) + [('-%s' % k, '-%s' % v) for k, v in self.choices]
        return self.dj_field_class(
            required=required,
            choices=choices,
            **self.kwargs
        )


class SortField(SortFieldMixin, sform.ChoiceField):

    dj_field_class = forms.ChoiceField


class MultipleSortField(SortFieldMixin, sform.MultipleChoiceField):

    dj_field_class = forms.MultipleChoiceField


class UserSuggestField(SuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=get_user_model().objects.all(),
            to_field_name='username',
            label_fields=['first_name', 'last_name'],
            *args, **kwargs
        )


class UserMultipleSuggestField(MultipleSuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=get_user_model().objects.all(),
            to_field_name='username',
            label_fields={
                'caption': ['first_name', 'last_name'],
                'extra_fields': [],
            },
            **kwargs
        )


class UserChoiceField(sform.SuggestField):
    """
    Гибрид modelchoice и suggest
    Принимает логин вместо id
    """
    def __init__(self, *args, **kwargs):
        self.empty_label = kwargs.pop('empty_label', None)
        self.label_extractor = kwargs.pop('label_extractor', default_user_label_extractor)
        super().__init__(
            to_field_name='username',
            label_fields=['first_name', 'last_name'],
            *args, **kwargs
        )

    def structure_as_dict(self, *args, **kwargs):
        field_dict = super().structure_as_dict(*args, **kwargs)
        field_dict['type'] = 'modelchoice'
        field_dict['types'] = 'modelchoice'

        def choice_gen():
            if self.empty_label is not None:
                yield ('', self.empty_label)
            for obj in self.queryset.all():
                yield (obj.username, self.label_extractor(obj))

        field_dict['choices'] = [
            {'value': v, 'label': n} for v, n in choice_gen()
        ]

        return field_dict


class CityMultipleSuggestField(MultipleSuggestField):

    def __init__(self, only_active=True, *args, **kwargs):
        manager = City.active if only_active else City.objects
        super().__init__(
            queryset=manager.all(),
            to_field_name='id',
            label_fields={
                'caption': ['name_ru'],
                'extra_fields': [],
            },
            **kwargs
        )

    def get_label_fields(self, label_fields=None):
        return super().get_label_fields(get_name_field())


class CityMultipleChoiceField(ChoiceDataMixin, sform.ModelMultipleChoiceField):

    type_name = 'modelmultiplechoice'
    choice_serializer_class = CityAsChoiceSerializer
    publications_path = 'vacancies__publications'

    def __init__(self, *, only_active=True, exclude_remote=False, **kwargs):
        self.only_active = only_active
        self.exclude_remote = exclude_remote
        super().__init__(queryset=self.get_queryset(), **kwargs)

    def get_queryset(self):
        queryset = City.active.all() if self.only_active else City.objects.all()
        if self.exclude_remote:
            queryset = queryset.exclude(id=settings.CITY_HOMEWORKER_ID)
        return queryset

    def get_queryset_for_structure(self):
        lang = get_publications_lang()
        queryset = (
            self.get_queryset()
            .select_related('country')
            .annotate(
                external_publications_count=count_external_publications_annotation(
                    publications_path=self.publications_path,
                    lang=lang,
                ),
            )
        )
        return queryset


class OfficeMultipleChoiceField(sform.ModelMultipleChoiceField):

    type_name = 'modelmultiplechoice'

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=self.get_queryset(),
            label_extractor=lambda x: x.name,
            *args, **kwargs
        )

    def get_queryset(self):
        return (
            Office.objects
            .alive()
            .exclude_groups()
            .order_by('name_ru')
        )


class WorkModeMultipleChoiceField(sform.ModelMultipleChoiceField):
    type_name = 'modelmultiplechoice'

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=WorkMode.objects.all(),
            label_extractor=lambda x: x.slug,
            *args, **kwargs,
        )


class CurrencySuggestField(SuggestField):

    def __init__(self, **kwargs):
        kwargs.setdefault('to_field_name', 'id')
        super().__init__(
            queryset=Currency.active.all(),
            label_fields=['code'],
            **kwargs
        )


class SkillSuggestField(SuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Skill.objects.all(),
            label_fields=['name'],
            *args, **kwargs
        )


class SkillMultipleSuggestField(MultipleSuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Skill.objects.all(),
            to_field_name='id',
            label_fields={
                'caption': ['name'],
                'extra_fields': [],
            },
            **kwargs
        )


class SkillMultipleChoiceField(ChoiceDataMixin, sform.ModelMultipleChoiceField):

    type_name = 'modelmultiplechoice'
    choice_serializer_class = SkillAsChoiceSerializer
    publications_path = 'vacancies__publications'

    def __init__(self, **kwargs):
        super().__init__(queryset=self.get_queryset(), **kwargs)

    def get_queryset(self):
        return Skill.objects.filter(is_public=True)

    def get_queryset_for_structure(self):
        lang = get_publications_lang()
        queryset = (
            self.get_queryset()
            .annotate(
                external_publications_count=count_external_publications_annotation(
                    publications_path=self.publications_path,
                    lang=lang,
                ),
            )
        )
        return queryset


class ProfessionSuggestField(SuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Profession.active.all(),
            label_fields=['name'],
            *args, **kwargs
        )

    def get_label_fields(self, label_fields=None):
        return super().get_label_fields(get_localized_name_field())


class ProfessionMultipleSuggestField(MultipleSuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Profession.active.all(),
            to_field_name='id',
            label_fields={
                'caption': ['name'],
                'extra_fields': [],
            },
            **kwargs
        )

    def get_label_fields(self, label_fields=None):
        return super().get_label_fields(get_localized_name_field())


class ProfessionChoiceField(sform.ModelChoiceField):

    type_name = 'modelchoice'

    def __init__(self, queryset=None, only_active=True, *args, **kwargs):
        manager = Profession.active if only_active else Profession.objects
        queryset = queryset if queryset is not None else manager.all()
        kwargs.setdefault('label_extractor', lambda x: x.localized_name)
        super().__init__(queryset, *args, **kwargs)

    @cached_property
    def prof_sphere_map(self):
        return {
            item.id: item.professional_sphere_id
            for item in self.queryset
        }

    def structure_as_dict(self, *args, **kwargs):
        field_dict = super().structure_as_dict(*args, **kwargs)
        for choice in field_dict['choices']:
            choice['professional_sphere_id'] = self.prof_sphere_map.get(choice['value'])
        return field_dict


class ProfessionMultipleChoiceField(ProfessionChoiceField, sform.ModelMultipleChoiceField):

    type_name = 'modelmultiplechoice'

    def __init__(self, *args, **kwargs):
        kwargs.setdefault('default', [])
        super().__init__(*args, **kwargs)


class BaseProfessionMultipleChoiceWithDataField(ChoiceDataMixin, sform.ModelMultipleChoiceField):

    type_name = 'modelmultiplechoice'
    publications_path = None

    def get_queryset_for_structure(self):
        assert self.publications_path
        lang = get_publications_lang()
        queryset = (
            self.get_queryset()
            .annotate(
                external_publications_count=count_external_publications_annotation(
                    publications_path=self.publications_path,
                    lang=lang,
                ),
            )
        )
        return queryset

    def structure_as_dict(self, *args, **kwargs):
        structure = super().structure_as_dict(*args, **kwargs)
        professions = structure['choices']
        sphere_priorities = self.get_professional_sphere_priorities(professions)
        for profession in professions:
            sphere = profession['data']['professional_sphere']
            sphere['priority'] = sphere_priorities[sphere['id']]

        return structure

    def get_professional_sphere_priorities(self, serialized_professions: list):
        """
        Считаем priority для professional_sphere как сумму приоритетов профессий
        """
        sphere_priorities = defaultdict(int)

        for profession in serialized_professions:
            sphere_id = profession['data']['professional_sphere']['id']
            profession_priority = profession['data']['priority']
            sphere_priorities[sphere_id] += profession_priority

        return sphere_priorities


class ProfessionMultipleChoiceWithDataField(BaseProfessionMultipleChoiceWithDataField):

    choice_serializer_class = ProfessionAsChoiceSerializer
    publications_path = 'vacancies__publications'

    def __init__(self, *, only_active=True, **kwargs):
        self.only_active = only_active
        manager = Profession.active if self.only_active else Profession.objects
        queryset = manager.exclude(id__in=settings.JOBS_FORBIDDEN_PROFESSIONS)
        super().__init__(queryset=queryset, **kwargs)

    def get_queryset_for_structure(self):
        return super().get_queryset_for_structure().select_related('professional_sphere')


class PublicProfessionMultipleChoiceWithDataField(BaseProfessionMultipleChoiceWithDataField):

    choice_serializer_class = PublicProfessionAsChoiceSerializer
    publications_path = 'professions__vacancies__publications'

    def __init__(self, *args, **kwargs):
        queryset = PublicProfession.objects.all()
        super().__init__(queryset=queryset, **kwargs)

    def get_queryset_for_structure(self):
        queryset = super().get_queryset_for_structure()
        return queryset.filter(is_active=True).select_related('public_professional_sphere')


class CandidateSuggestField(SuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Candidate.objects.alive(),
            label_fields=['first_name', 'last_name'],
            *args, **kwargs
        )


class CandidateMultipleSuggestField(MultipleSuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Candidate.objects.alive(),
            to_field_name='id',
            label_fields={
                'caption': ['first_name', 'last_name'],
                'extra_fields': ['status', 'first_name', 'last_name'],
            },
            **kwargs
        )


class CandidateDuplicateChoiceField(sform.ModelChoiceField):

    def __init__(self, queryset, *args, **kwargs):
        self.details = {}
        super().__init__(
            queryset=queryset,
            label_extractor=self.extract_label,
            *args, **kwargs
        )

    def structure_as_dict(self, *args, **kwargs):
        structure = super().structure_as_dict(*args, **kwargs)
        structure['type'] = 'modelchoice'
        for choice in structure['choices']:
            if isinstance(choice.get('label'), dict):
                old_label = choice.pop('label')
                choice.update(old_label)
        return structure

    def extract_label(self, obj):
        obj_details = self.details.get(obj.id)
        return CandidateDuplicateAsChoiceSerializer({
            'candidate': obj,
            'details': obj_details.to_dict() if obj_details else {},
        }).data


class ConsiderationSuggestField(SuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Consideration.objects.all(),
            label_fields=['id'],
            *args, **kwargs
        )


class ConsiderationChoiceField(ChoiceDataMixin, sform.ModelChoiceField):

    type_name = 'modelchoice'
    choice_serializer_class = ConsiderationAsChoiceSerializer


class VacancySuggestField(SuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Vacancy.objects.all(),
            label_fields=['name'],
            *args, **kwargs
        )


class VacancyMultipleSuggestField(MultipleSuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Vacancy.objects.all(),
            to_field_name='id',
            label_fields={
                'caption': ['name'],
                'extra_fields': ['status', 'name'],
            },
            **kwargs
        )


class PublicationMultipleSuggestField(MultipleSuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Vacancy.unsafe.published(),
            to_field_name='id',
            label_fields={
                'caption': ['publication_title'],
            },
            **kwargs
        )

    def structure_as_dict(self, *args, **kwargs):
        field_dict = super().structure_as_dict(*args, **kwargs)
        field_dict['types'] = ['publication']
        return field_dict


class ApplicationSuggestField(SuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Application.objects.all(),
            label_fields=['id'],
            *args, **kwargs
        )


class ApplicationChoiceField(sform.ModelChoiceField):

    def __init__(self, queryset, *args, **kwargs):
        queryset = queryset.select_related(
            'consideration',
            'vacancy',
        )
        super().__init__(
            queryset=queryset,
            label_extractor=lambda x: ApplicationAsChoiceSerializer(x).data,
            *args, **kwargs
        )

    def structure_as_dict(self, *args, **kwargs):
        structure = super().structure_as_dict(*args, **kwargs)
        structure['type'] = 'modelchoice'
        for choice in structure['choices']:
            if isinstance(choice.get('label'), dict):
                choice['application'] = choice.pop('label')
                choice['label'] = choice['application'].pop('label')
        return structure


class AttachmentMultipleSuggestField(MultipleSuggestField):

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

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

        if self.max_files and len(value) > self.max_files:
            raise ValidationError(message='max_files', code='max_files')

        return value

    def structure_as_dict(self, *args, **kwargs):
        result = (
            super()
            .structure_as_dict(*args, **kwargs)
        )
        result['type'] = 'attachments'
        if self.max_files:
            result['maxFiles'] = self.max_files
        return result


class PositionSuggestField(SuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Position.objects.alive(),
            label_fields=['name_ru'],
            *args, **kwargs
        )

    def get_label_fields(self, label_fields=None):
        return super().get_label_fields(get_name_field())


class ProblemCategoryMultipleSuggestField(MultipleSuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Category.objects.all(),
            to_field_name='id',
            label_fields={
                'caption': ['name'],
                'extra_fields': [],
            },
            **kwargs
        )


class CategoryModelChoiceField(sform.ModelChoiceField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Category.objects.all(),
            label_extractor=lambda x: x.name,
            *args, **kwargs
        )

    def structure_as_dict(self, *args, **kwargs):
        structure = super().structure_as_dict(*args, **kwargs)
        structure['type'] = 'modelchoice'
        return structure


class CategoryModelMultipleChoiceField(sform.ModelMultipleChoiceField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Category.objects.all(),
            label_extractor=lambda x: x.name,
            *args, **kwargs
        )

    def structure_as_dict(self, *args, **kwargs):
        result = (
            super()
            .structure_as_dict(*args, **kwargs)
        )
        result['type'] = 'modelmultiplechoice'
        return result


class ProblemSuggestField(SuggestField):

    def __init__(self, skip_alive_check=False, *args, **kwargs):
        manager = Problem.objects if skip_alive_check else Problem.alive_objects
        super().__init__(
            queryset=manager.all(),
            label_fields=['summary'],
            *args, **kwargs
        )


class InterviewSuggestField(SuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Interview.objects.all(),
            label_fields=['section'],
            *args, **kwargs
        )


class InterviewMultipleChoiceField(sform.ModelMultipleChoiceField):

    def __init__(self, queryset, *args, **kwargs):
        queryset = queryset.select_related('interviewer')
        label_extractor = lambda x: InterviewAsChoiceSerializer(x).data
        super().__init__(
            queryset=queryset,
            label_extractor=label_extractor,
            *args, **kwargs
        )

    def structure_as_dict(self, *args, **kwargs):
        structure = super().structure_as_dict(*args, **kwargs)
        structure['type'] = 'modelmultiplechoice'
        for choice in structure['choices']:
            if isinstance(choice.get('label'), dict):
                choice['interview'] = choice.pop('label')
                choice['label'] = choice['interview'].pop('label')
        return structure


class PresetSuggestField(SuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Preset.objects.all(),
            label_fields=['name'],
            *args, **kwargs
        )


class ChallengeSuggestField(SuggestField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            queryset=Challenge.objects.all(),
            label_fields=['id'],
            *args, **kwargs
        )


class PositiveIntegerField(sform.Field):

    def get_dj_field(self, required=False):
        return IntegerField(required=required, min_value=0, **self.kwargs)

    def structure_as_dict(self, *args, **kwargs):
        result = super().structure_as_dict(*args, **kwargs)
        result['type'] = 'integer'
        return result


class PositiveDecimalField(sform.Field):

    def get_dj_field(self, required=False):
        return DecimalField(required=required, min_value=0, **self.kwargs)

    def structure_as_dict(self, *args, **kwargs):
        result = super().structure_as_dict(*args, **kwargs)
        result['type'] = 'decimal'
        return result


class DepartmentSuggestField(SuggestField):

    def __init__(self, queryset=None, only_active=True, **kwargs):
        queryset = Department.objects.all() if queryset is None else queryset
        filter_data = {'is_deleted': False} if only_active else {}
        kwargs.setdefault('to_field_name', 'id')
        super().__init__(
            queryset=queryset.filter(**filter_data),
            label_fields=['name'],
            **kwargs,
        )

    def get_label_fields(self, label_fields=None):
        return super().get_label_fields(get_localized_name_field())


class ValueStreamSuggestField(SuggestField):

    def __init__(self, queryset=None, only_active=True, **kwargs):
        queryset = ValueStream.objects.all() if queryset is None else queryset
        filter_data = {'is_active': True} if only_active else {}
        kwargs.setdefault('to_field_name', 'id')
        super().__init__(
            queryset=queryset.filter(**filter_data),
            label_fields=['name'],
            **kwargs,
        )


class GeographySuggestField(SuggestField):

    def __init__(self, queryset=None, only_active=True, **kwargs):
        queryset = Geography.objects.all() if queryset is None else queryset
        filter_data = {'is_deleted': False} if only_active else {}
        kwargs.setdefault('to_field_name', 'id')
        super().__init__(
            queryset=queryset.filter(**filter_data),
            label_fields=['name'],
            **kwargs,
        )

    def get_label_fields(self, label_fields=None):
        return super().get_label_fields(get_name_field())


class ServiceMultipleSuggestField(MultipleSuggestField):

    def __init__(self, only_active=True, **kwargs):
        filter_data = {'is_deleted': False} if only_active else {}
        kwargs.setdefault('to_field_name', 'id')
        super().__init__(
            queryset=Service.objects.filter(**filter_data),
            label_fields={
                'caption': ['name'],
                'extra_fields': [],
            },
            **kwargs
        )


class PublicServiceMultipleChoiceField(ChoiceDataMixin, sform.ModelMultipleChoiceField):

    type_name = 'modelmultiplechoice'
    choice_serializer_class = PublicServiceAsChoiceSerializer
    publications_path = 'publications'

    def __init__(self, *, only_active=True, **kwargs):
        self.only_active = only_active
        super().__init__(queryset=self.get_queryset(), **kwargs)

    def get_queryset(self):
        queryset = PublicService.objects.all()
        if self.only_active:
            queryset = queryset.filter(is_active=True)
        return queryset

    def get_queryset_for_structure(self):
        lang = get_publications_lang()
        queryset = (
            self.get_queryset()
            .annotate(
                external_publications_count=count_external_publications_annotation(
                    publications_path=self.publications_path,
                    lang=lang,
                ),
            )
        )
        return queryset


class MessageSuggestField(SuggestField):

    def get_default_queryset(self):
        return Message.objects.alive()

    def __init__(self, queryset=None, *args, **kwargs):
        if queryset is None:
            queryset = self.get_default_queryset()
        super().__init__(
            queryset=queryset,
            label_fields=['id'],
            **kwargs
        )


class ReminderMessageSuggestField(MessageSuggestField):
    """
    Саджест по сообщениям для напоминаний.
    Напоминания можно создавать на 2 вида сообщений:
    - заметки
    - внутреняя коммуникация (без брифа)
    """
    def get_default_queryset(self):
        reminder_message_types = (
            MESSAGE_TYPES.note,
            MESSAGE_TYPES.internal,
        )
        return (
            Message.objects
            .alive()
            .filter(type__in=reminder_message_types)
        )


class ConditionallyRequiredFieldsMixin:
    """
    Миксин, который добавляет возможность использовать
    условно-обязательные поля.

    Например, поле "field" должно быть обязательным при условии,
    что field2 == True. Указываем данную зависимость в параметре:
    CONDITIONALLY_REQUIRED = {
        'field': ('field2', True),
    }
    """

    CONDITIONALLY_REQUIRED = {}

    def _mark_required(self, field_name):
        self.fields_state[field_name] = sform.REQUIRED

    def get_field_state(self, name):
        if name in self.CONDITIONALLY_REQUIRED:
            related_field_name, value = self.CONDITIONALLY_REQUIRED[name]
            related_value = self.cleaned_data.get(related_field_name)
            container_types = (list, tuple, set, Choices)
            is_required = (
                isinstance(value, container_types) and related_value in value
                or related_value == value
            )
            if is_required:
                self._mark_required(name)

        return super().get_field_state(name)


# TODO: вынести это в sform.
# Сейчас state=REQUIRED никак не меняет поведение булеана
class BooleanField(sform.BooleanField):
    """
    Булевое поле, которое требует true, если required,
    как и стандартное поведение полей в django формах.
    """
    def get_dj_field(self, required=False):
        return forms.BooleanField(required=required, **self.kwargs)


class AddProblemsFromPresetForm(sform.SForm):

    preset = PresetSuggestField(state=sform.REQUIRED)


class BaseMetaForm(sform.SForm):

    def meta_as_dict(self):
        return {}

    def as_dict(self):
        result = super().as_dict()
        result['meta'] = self.meta_as_dict()
        return result


class BasePartialForm(sform.SForm):
    """
    Форма с возможностью получения подмножества полей
    """
    def as_dict(self):
        if self.partial_fields:
            self.fields = OrderedDict([
                (k, v) for k, v in self.fields.items()
                if k in self.partial_fields
            ])

        return super().as_dict()


class CounterChoiceFieldMixin:
    """
    Используется для фильтр-форм.

    Миксин добавляющий к вариантам в choice-полях,
    кол-во элементов с таким критерием в фильтруемой коллекции.
    В качестве источника данных используется словарь `counts`
    вида `вариант: кол-во`.

    В итоге для каждого варианта[1] в структуре поля получим:
    {
        'label': 'label',
        'value': 'value',
        'count': counts.get('value', 0)
    }

    [1] Кроме варианта с пустым значением,
    который у нас обычно используется как "Все",
    там `count` = сумме всех других count'ов.
    """
    def add_counts_to_structure(self, structure):
        empty_choice = structure['choices'][0]
        empty_choice['count'] = 0

        for choice in structure['choices']:
            count = self.counts.get(choice['value'], 0)
            if choice['value'] != '':
                choice['count'] = count
            empty_choice['count'] += count

        return structure


class CounterChoiceField(CounterChoiceFieldMixin, sform.ChoiceField):

    def __init__(self, choices, counts=None, *args, **kwargs):
        super().__init__(choices, *args, **kwargs)
        self.counts = counts or {}

    def structure_as_dict(self, *args, **kwargs):
        structure = super().structure_as_dict(*args, **kwargs)
        structure['type'] = 'choice'
        return self.add_counts_to_structure(structure)


class CounterModelChoiceField(CounterChoiceFieldMixin, sform.ModelChoiceField):

    def __init__(self, queryset, counts=None, *args, **kwargs):
        super().__init__(queryset, *args, **kwargs)
        self.counts = counts or {}

    def structure_as_dict(self, *args, **kwargs):
        structure = super().structure_as_dict(*args, **kwargs)
        structure['type'] = 'modelchoice'
        return self.add_counts_to_structure(structure)


class NumericCharField(sform.CharField):
    """
    Строковое поле, где допустимы только цифры
    """
    def clean(self, *args, **kwargs):
        value = super().clean(*args, **kwargs)
        if value:
            RegexValidator(r'^\d+$', code='only_digits_are_allowed')(value)
        return value

    def structure_as_dict(self, *args, **kwargs):
        structure = super().structure_as_dict(*args, **kwargs)
        structure['type'] = 'char'
        structure['minlength'] = self.kwargs['min_length']
        return structure


class TimeZoneChoiceField(sform.ChoiceField):

    def __init__(self, *args, **kwargs):
        super().__init__(
            choices=Choices(*pytz.all_timezones),
            *args,
            **kwargs
        )


class ExtendedChoiceField(sform.ChoiceField):
    field_type_name = 'choice'

    def __init__(self, extra=None, field_type_name='choice', *args, **kwargs):
        self.extra = extra if extra else {}
        self.field_type_name = field_type_name
        super().__init__(*args, **kwargs)

    @property
    def type_name(self):
        if self.field_type_name:
            return self.field_type_name
        return super().type_name

    def structure_as_dict(self, *args, **kwargs):
        field_dict = super().structure_as_dict(*args, **kwargs)
        for choice in field_dict['choices']:
            for key in self.extra:
                values = self.extra[key]
                choice[key] = values.get(choice['value'], None)
        return field_dict
