# -*- coding: utf-8 -*-
import random
from collections import (
    defaultdict,
    namedtuple,
    OrderedDict,
)
from django.conf import settings
from django.utils.translation import ugettext as _
from json import loads as json_loads

from events.accounts.models import User
from events.captcha.captcha import Captcha
from events.common_app.middleware import get_current_request
from events.data_sources.sources import data_sources_by_name
from events.surveyme.answer import (
    as_dict as answer_as_dict,
    get_last_answer,
    ChoiceValue,
    DateRangeValue,
    TitleValue,
)
from events.surveyme.models import (
    Survey,
    SurveyAgreement,
    SurveyQuestion,
    SurveyQuestionChoice,
    SurveyQuestionMatrixTitle,
    SurveyQuestionShowConditionNode,
    SurveyQuestionShowConditionNodeItem,
    SurveySubmitConditionNode,
    SurveySubmitConditionNodeItem,
    SurveyText,
)
from events.surveyme_keys.models import Key as SurveyKey


def parse_json(s):
    if s is None:
        return None
    try:
        return json_loads(s)
    except (TypeError, ValueError):
        return None


def create_class(cls, args):
    return cls(**dict(zip(cls._fields, args)))


TRANSLATIONS_SUPPORT = settings.APP_TYPE in ('forms_ext', 'forms_ext_admin', 'forms_int')


def get_fallback_language(language):
    languages = settings.MODELTRANSLATION_FALLBACK_LANGUAGES.get(language)
    if languages:
        return languages[0]


def get_translated(obj, name, language, default_language):
    value = getattr(obj, name, '')
    if not TRANSLATIONS_SUPPORT:
        return value

    if language == default_language:
        return value

    translations = getattr(obj, 'translations', None)
    if not isinstance(translations, dict):
        return value

    if name not in translations:
        return value

    translated_name = translations[name]
    if not isinstance(translated_name, dict):
        return value

    if language not in translated_name:
        language = get_fallback_language(language)
        if language not in translated_name:
            return value

    return translated_name[language]


SurveyData = namedtuple('SurveyData', [
    'pk',  # int|str
    'slug',  # str
    'is_published_external',  # bool
    'is_public',  # bool
    'captcha_type',  # str
    'captcha_display_mode',  # str
    'name',  # str
    'metrika_counter_code',  # str
    'is_only_for_iframe',  # bool
    'is_allow_answer_editing',  # bool
    'is_allow_multiple_answers',  # bool
    'is_allow_answer_versioning',  # bool
    'need_auth_to_answer',  # bool
    'extra',  # dict
    'styles',  # dict
    'image_page',  # str
    'image_form',  # str
    'group_pk',  # int
    'group_metrika_counter_code',  # str
    'language',  # str
    'translations',  # Dict[str, Dict[str, str]]
    'dir_id',  # str
])


def create_survey_class(args):
    return create_class(SurveyData, args)


def get_survey(survey_id):
    survey_qs = (
        Survey.objects.using(settings.DATABASE_ROLOCAL)
        .select_related('survey_template', 'group')
    )
    fields = [
        'pk',
        'slug',
        'is_published_external',
        'is_public',
        'captcha_type',
        'captcha_display_mode',
        'name',
        'metrika_counter_code',
        'is_only_for_iframe',
        'is_allow_answer_editing',
        'is_allow_multiple_answers',
        'is_allow_answer_versioning',
        'need_auth_to_answer',
        'extra',
        'styles_template__styles',
        'styles_template__image_page__image',
        'styles_template__image_form__image',
        'group__pk',
        'group__metrika_counter_code',
        'language',
        'translations',
    ]
    if settings.IS_BUSINESS_SITE:
        survey_qs = survey_qs.select_related('org')
        fields.append('org__dir_id')
    else:
        fields.append('org_id')
    survey_qs = survey_qs.values_list(*fields)
    if settings.IS_BUSINESS_SITE:
        kwargs = {'pk': survey_id}
    else:
        if isinstance(survey_id, str) and not survey_id.isdigit():
            kwargs = {'slug': survey_id}
        else:
            kwargs = {'pk': survey_id}
    return create_survey_class(survey_qs.get(**kwargs))


TextData = namedtuple('TextData', [
    'slug',  # str
    'value',  # str
    'translations',  # Dict[str, Dict[str, str]]
])


def create_text_class(args):
    return create_class(TextData, args)


def get_texts(survey_id):
    return list(
        create_text_class(args)
        for args in SurveyText.objects.using(settings.DATABASE_ROLOCAL)
        .filter(survey_id=survey_id)
        .values_list(
            'slug',
            'value',
            'translations',
        )
    )


AgreementData = namedtuple('AgreementData', [
    'text',  # str
    'is_required',  # bool
    'slug',  # str
    'translations',  # Dict[str, Dict[str, str]]
])


def create_agreement_class(args):
    return create_class(AgreementData, args)


def get_agreements(survey_id):
    if settings.IS_BUSINESS_SITE:
        return []
    return list(
        create_agreement_class(args)
        for args in SurveyAgreement.objects.using(settings.DATABASE_ROLOCAL)
        .filter(surveys__pk=survey_id)
        .values_list(
            'text',
            'is_required',
            'slug',
            'translations',
        )
    )


QuestionData = namedtuple('QuestionData', [
    'pk',  # int
    'label',  # str
    'page',  # int
    'position',  # int
    'group_id',  # int
    'answer_type_id',  # int
    'answer_type_slug',  # str
    'image',  # str
    'initial',  # str
    'slug',  # str
    'data_source',  # str
    'data_source_params',  # str
    'param_help_text',  # str
    'is_hidden',  # bool
    'is_required',  # bool
    'is_allow_multiple_choice',  # bool
    'is_disabled_init_item',  # bool
    'suggest_choices',  # bool
    'modify_choices',  # str
    'widget',  # str
    'min',  # int
    'max',  # int
    'max_file_size',  # int
    'max_files_count',  # int
    'hint_data_source',  # str
    'hint_data_source_params',  # dict
    'date_field_type',  # str
    'date_field_min',  # date
    'date_field_max',  # date
    'payment',  # Dict[str, str]
    'is_section_header',  # bool
    'translations',  # Dict[str, Dict[str, str]]
])


def create_question_class(args):
    return create_class(QuestionData, args)


def get_questions(survey_id):
    return OrderedDict(
        (args[0], create_question_class(args))
        for args in SurveyQuestion.objects.using(settings.DATABASE_ROLOCAL)
        .select_related('answer_type', 'label_image')
        .filter(survey_id=survey_id)
        .order_by('page', 'position')
        .values_list(
            'pk',
            'label',
            'page',
            'position',
            'group_id',
            'answer_type__pk',
            'answer_type__slug',
            'label_image__image',
            'initial',
            'param_slug',
            'param_data_source',
            'param_data_source_params',
            'param_help_text',
            'param_is_hidden',
            'param_is_required',
            'param_is_allow_multiple_choice',
            'param_is_disabled_init_item',
            'param_suggest_choices',
            'param_modify_choices',
            'param_widget',
            'param_min',
            'param_max',
            'param_max_file_size',
            'param_max_files_count',
            'param_hint_data_source',
            'param_hint_data_source_params',
            'param_date_field_type',
            'param_date_field_min',
            'param_date_field_max',
            'param_payment',
            'param_is_section_header',
            'translations',
        )
    )


def check_for_choices(questions):
    for question in questions:
        if (
            question.answer_type_slug == 'answer_choices'
            and question.data_source == 'survey_question_choice'
        ):
            return True
    return False


ChoiceData = namedtuple('ChoiceData', [
    'pk',  # int
    'label',  # str
    'slug',  # str
    'image',  # str
    'translations',  # Dict[str, Dict[str, str]]
])


def create_choice_class(args):
    return create_class(ChoiceData, args)


def get_choices(survey_id, questions):
    if check_for_choices(questions.values()):
        choices = defaultdict(list)
        choices_qs = (
            SurveyQuestionChoice.objects.using(settings.DATABASE_ROLOCAL)
            .filter(survey_question__survey_id=survey_id, is_hidden=False)
            .order_by('position')
            .values_list(
                'survey_question_id',
                'pk', 'label', 'slug', 'label_image__image',
                'translations',
            )
        )
        for question_id, *args in choices_qs:
            choices[question_id].append(create_choice_class(args))
        return choices


def check_for_titles(questions):
    for question in questions:
        if (
            question.answer_type_slug == 'answer_choices'
            and question.data_source == 'survey_question_matrix_choice'
        ):
            return True
    return False


TitleData = namedtuple('TitleData', [
    'pk',  # int
    'label',  # str
    'position',  # int
    'type',  # str
    'translations',  # Dict[str, Dict[str, str]]
])


def create_title_class(args):
    return create_class(TitleData, args)


def get_titles(survey_id, questions):
    if check_for_titles(questions.values()):
        titles = defaultdict(list)
        titles_qs = (
            SurveyQuestionMatrixTitle.objects.using(settings.DATABASE_ROLOCAL)
            .filter(survey_question__survey_id=survey_id)
            .order_by('position')
            .values_list(
                'survey_question_id',
                'pk', 'label', 'position', 'type',
                'translations',
            )
        )
        for question_id, *args in titles_qs:
            titles[question_id].append(create_title_class(args))
        return titles


ConditionItemData = namedtuple('ConditionItemData', [
    'question_id',  # int
    'operator',  # str
    'condition',  # str
    'value',  # str
])


def create_condition_item_class(args):
    return create_class(ConditionItemData, args)


def get_show_conditions(survey_id):
    nodes = [
        (args[0], args[1])
        for args in SurveyQuestionShowConditionNode.objects.using(settings.DATABASE_ROLOCAL)
        .filter(survey_question__survey_id=survey_id, survey_question__is_deleted=False)
        .order_by('position')
        .values_list(
            'pk',
            'survey_question_id',
        )
    ]
    if nodes:
        items_qs = (
            SurveyQuestionShowConditionNodeItem.objects.using(settings.DATABASE_ROLOCAL)
            .filter(
                survey_question_show_condition_node__survey_question__survey_id=survey_id,
                survey_question__is_deleted=False,
            )
            .order_by('position')
            .values_list(
                'survey_question_show_condition_node_id',
                'survey_question_id',
                'operator',
                'condition',
                'value',
            )
        )
        items = defaultdict(list)
        for node_id, *args in items_qs:
            items[node_id].append(create_condition_item_class(args))

        conditions = defaultdict(OrderedDict)
        for node_id, question_id in nodes:
            conditions[question_id][node_id] = items.get(node_id)

        return conditions


def get_submit_conditions(survey_id):
    nodes = [
        args[0]
        for args in SurveySubmitConditionNode.objects.using(settings.DATABASE_ROLOCAL)
        .filter(survey_id=survey_id)
        .order_by('position')
        .values_list(
            'pk',
        )
    ]
    if nodes:
        items_qs = (
            SurveySubmitConditionNodeItem.objects.using(settings.DATABASE_ROLOCAL)
            .filter(
                survey_submit_condition_node__survey_id=survey_id,
                survey_question__is_deleted=False,
            )
            .order_by('position')
            .values_list(
                'survey_submit_condition_node_id',
                'survey_question_id',
                'operator',
                'condition',
                'value',
            )
        )
        items = defaultdict(list)
        for node_id, *args in items_qs:
            items[node_id].append(create_condition_item_class(args))

        conditions = OrderedDict()
        for node_id in nodes:
            conditions[node_id] = items.get(node_id)

        return conditions


def get_image(path):
    if path:
        return {
            'links': {
                size: '{host}get-{namespace}/{path}/{size}'.format(
                    host=settings.AVATARS_HOST,
                    namespace=settings.IMAGE_NAMESPACE,
                    path=path,
                    size=size,
                )
                for size in settings.IMAGE_SIZES_AS_STR
            }
        }


def get_host():
    request = get_current_request()
    if request:
        return request.get_host()
    return 'localhost'


class Field:
    slug = None
    widget_class = 'TextInput'

    def __init__(self, form_class):
        self._form_class = form_class

    @property
    def user(self):
        return self._form_class.user

    @property
    def survey(self):
        return self._form_class.survey

    @property
    def questions(self):
        return self._form_class.questions

    @property
    def choices(self):
        return self._form_class.choices

    @property
    def titles(self):
        return self._form_class.titles

    @property
    def last_answer(self):
        return self._form_class.last_answer

    def get_translated_field(self, *args, **kwargs):
        return self._form_class.get_translated_field(*args, **kwargs)

    def get_group_slug(self, question):
        return self._form_class.get_group_slug(question)

    def get_question_slug(self, question_id):
        return self._form_class.get_question_slug(question_id)

    def get_show_conditions(self, question):
        return self._form_class.get_form_show_conditions(question)

    def get_field_name(self, question, group_idx=0, format='%s'):
        field_name = format % question.slug
        if question.group_id:
            return '%s__%s' % (field_name, group_idx)
        return field_name

    def get_errors(self):
        return []

    def get_error_messages(self):
        return {
            'invalid': 'invalid',
            'required': 'required',
        }

    def get_other_data(self, question):
        other_data = {}
        show_conditions = self.get_show_conditions(question)
        if show_conditions:
            other_data['show_conditions'] = show_conditions
        return other_data

    def get_tags_data(self, question, group_idx):
        return []

    def get_hints_data(self, question):
        if question.hint_data_source:
            return self._get_data_source(question.hint_data_source, question.hint_data_source_params)

    def _get_data_source(self, data_source, data_source_params):
        data_source_class = data_sources_by_name[data_source]
        data_source_instance = data_source_class(user=self.user)
        if data_source_instance.is_with_pagination:
            return self._get_with_pagination(data_source, data_source_params)
        else:
            return self._get_without_pagination(data_source_instance)

    def _get_with_pagination(self, data_source, data_source_params):
        return {
            'is_with_pagination': True,
            'filters': self.get_filters(data_source_params),
            'uri': self.get_uri(data_source),
        }

    def _get_without_pagination(self, data_source_instance):
        serializer = data_source_instance.serializer_class(
            data_source_instance.get_filtered_queryset(filter_data={}),
            many=True,
        )
        return {
            'items': serializer.data,
        }

    def get_uri(self, data_source):
        return 'https://{host}/v1/data-source/{name}/'.format(
            host=get_host(),
            name=data_source.replace('_', '-'),
        )

    def get_filters(self, filter_params):
        if isinstance(filter_params, str):
            filter_params = parse_json(filter_params)
        params = filter_params or {}
        filters = []
        for ft in params.get('filters') or []:
            if ft:
                if 'filter' in ft:
                    filter_name = ft.pop('filter')
                    ft['param_name'] = filter_name['name']
                    value = ft.pop('value', None)
                    if value:
                        ft['value'] = value
                    field = ft.pop('field', None)
                    if field:
                        ft['field'] = self.get_question_slug(int(field))
                filters.append(ft)
        return filters

    def check_if_show_last_answer(self):
        return (
            self.survey.is_allow_answer_editing
            or self.survey.is_allow_answer_versioning
        )

    def get_initial(self, question):
        if question.initial:
            if settings.IS_TEST:
                return question.initial
            return parse_json(question.initial)

    def get_value(self, question, group_idx):
        value = None
        if self.check_if_show_last_answer():
            if self.last_answer:
                value = self.last_answer.get(question.pk)
                if question.group_id:
                    if isinstance(value, list) and len(value) > group_idx:
                        value = value[group_idx]
        if not value:
            value = self.get_initial(question)
        return value

    def get_label(self, question, language):
        return self.get_translated_field(question, 'label', language) or question.slug

    def get_field_data(self, question, group_idx, language):
        field_name = self.get_field_name(question, group_idx)
        field = {
            'name': field_name,
            'label': self.get_label(question, language),
            'help_text': self.get_translated_field(question, 'param_help_text', language),
            'is_hidden': question.is_hidden,
            'is_required': question.is_required,
            'page': question.page,
            'position': question.position,
            'type_slug': question.answer_type_slug,
            'group_slug': self.get_group_slug(question),
            'errors': self.get_errors(),
            'error_messages': self.get_error_messages(),
            'widget': self.widget_class,
            'label_image': get_image(question.image),
        }
        other_data = self.get_other_data(question)
        if other_data:
            field['other_data'] = other_data
        tags = self.get_tags_data(question, group_idx)
        if tags:
            name_format = '{}_{}' if len(tags) > 1 else '{}'
            for i, tag in enumerate(tags):
                tag['attrs']['name'] = name_format.format(field_name, i)
            field['tags'] = tags
        hints = self.get_hints_data(question)
        if hints:
            field['hints'] = hints
        return field

    def get_group_depth(self, question):
        group_depth = 1
        if self.check_if_show_last_answer():
            if self.last_answer and question.group_id:
                group_depth = max(len(self.last_answer.get(question.pk, [])), group_depth)
        return group_depth

    def as_dict(self, question, language):
        for group_idx in range(self.get_group_depth(question)):
            yield self.get_field_data(question, group_idx, language)


class FieldGroup(Field):
    slug = 'answer_group'
    widget_class = 'GroupWidget'


class FieldStatement(Field):
    slug = 'answer_statement'
    widget_class = 'EmptyWidget'

    def get_other_data(self, question):
        other_data = super().get_other_data(question)
        other_data.update({
            'is_section_header': question.is_section_header,
            'widget': 'statement'
        })
        return other_data


class FieldShortText(Field):
    slug = 'answer_short_text'

    def get_tags_data(self, question, group_idx):
        return [{
            'tag': 'input',
            'attrs': {
                'type': 'text',
                'max': question.max,
                'min': question.min,
                'maxlength': 255,
                'value': self.get_value(question, group_idx),
            },
        }]


class FieldName(Field):
    slug = 'answer_name'

    def _get_fio(self):
        if self.user:
            return self.user.get_name_and_surname()

    def get_value(self, question, group_idx):
        fio = self._get_fio()
        if fio:
            parts = fio.split(' ')
            if len(parts) > 1:
                return parts[1]
        return super().get_value(question, group_idx)

    def get_tags_data(self, question, group_idx):
        return [{
            'tag': 'input',
            'attrs': {
                'type': 'text',
                'value': self.get_value(question, group_idx),
            },
        }]


class FieldSurname(FieldName):
    slug = 'answer_surname'

    def get_value(self, question, group_idx):
        fio = self._get_fio()
        if fio:
            return fio.split(' ', 1)[0]
        return super().get_value(question, group_idx)

    def get_tags_data(self, question, group_idx):
        return [{
            'tag': 'input',
            'attrs': {
                'type': 'text',
                'value': self.get_value(question, group_idx),
            },
        }]


class FieldLongText(Field):
    slug = 'answer_long_text'

    def get_tags_data(self, question, group_idx):
        return [{
            'tag': 'textarea',
            'content': self.get_value(question, group_idx),
            'attrs': {
                'type': 'text',
                'max': question.max,
                'min': question.min,
                'cols': '40',
                'rows': '10',
            },
        }]


class FieldBoolean(Field):
    slug = 'answer_boolean'

    def get_other_data(self, question):
        other_data = super().get_other_data(question)
        other_data.update({
            'widget': 'checkbox',
        })
        return other_data

    def get_value(self, question, group_idx):
        value = super().get_value(question, group_idx)
        if bool(value):
            return 'checked'

    def get_tags_data(self, question, group_idx):
        return [{
            'tag': 'input',
            'attrs': {
                'type': 'checkbox',
                'checked': self.get_value(question, group_idx),
            },
        }]


class FieldNumber(Field):
    slug = 'answer_number'
    widget_class = 'NumberInput'

    def get_other_data(self, question):
        other_data = super().get_other_data(question)
        other_data.update({
            'widget': 'number',
        })
        return other_data

    def get_tags_data(self, question, group_idx):
        return [{
            'tag': 'input',
            'attrs': {
                'type': 'number',
                'max': question.max or 0x7fffffff,
                'min': question.min,
                'value': self.get_value(question, group_idx),
            },
        }]


class FieldEmail(Field):
    slug = 'answer_non_profile_email'

    def get_tags_data(self, question, group_idx):
        return [{
            'tag': 'input',
            'attrs': {
                'type': 'email',
                'maxlength': 255,
                'value': self.get_value(question, group_idx),
            },
        }]


class FieldUrl(Field):
    slug = 'answer_url'

    def get_tags_data(self, question, group_idx):
        return [{
            'tag': 'input',
            'attrs': {
                'type': 'url',
                'maxlength': 1024,
                'value': self.get_value(question, group_idx),
            },
        }]


class FieldPhone(Field):
    slug = 'answer_phone'

    def get_tags_data(self, question, group_idx):
        return [{
            'tag': 'input',
            'attrs': {
                'type': 'text',
                'value': self.get_value(question, group_idx),
            },
        }]


class FieldFiles(Field):
    slug = 'answer_files'

    def get_other_data(self, question):
        other_data = super().get_other_data(question)
        other_data.update({
            'widget': 'multifile',
            'max_files_count': question.max_files_count,
            'max_file_size': question.max_file_size,
        })
        return other_data

    def get_tags_data(self, question, group_idx):
        return [{
            'tag': 'input',
            'attrs': {
                'type': 'file',
                'multiple': True,
            },
        }]


class FieldDate(Field):
    slug = 'answer_date'
    date_type = 'date'
    widget_class = 'DateInput'

    def get_allowed_range(self, question):
        allowed_range = {}
        if question.date_field_min:
            allowed_range['from'] = question.date_field_min.isoformat()
        if question.date_field_max:
            allowed_range['to'] = question.date_field_max.isoformat()
        return allowed_range

    def get_other_data(self, question):
        other_data = super().get_other_data(question)
        other_data.update({
            'widget': self.date_type,
            'allowed_range': self.get_allowed_range(question),
        })
        return other_data

    def get_tags_data(self, question, group_idx):
        return [{
            'tag': 'input',
            'attrs': {
                'type': 'text',
                'value': self.get_value(question, group_idx),
            },
        }]


class FieldDateRange(FieldDate):
    slug = 'answer_date'
    date_type = 'daterange'
    widget_class = 'DateRangeFieldInput'

    def get_tags_data(self, question, group_idx):
        value = self.get_value(question, group_idx)
        return [{
            'tag': 'input',
            'attrs': {
                'type': 'text',
                'value': self.get_value_begin(value),
            },
        }, {
            'tag': 'input',
            'attrs': {
                'type': 'text',
                'value': self.get_value_end(value),
            },
        }]

    def _get_value_attr(self, value, attr_name):
        if value:
            if isinstance(value, DateRangeValue):
                return getattr(value, attr_name, None)
            if isinstance(value, dict):
                return value.get(attr_name)

    def get_value_begin(self, value):
        return self._get_value_attr(value, 'begin')

    def get_value_end(self, value):
        return self._get_value_attr(value, 'end')


class FieldPayment(Field):
    slug = 'answer_payment'
    widget_class = 'PaymentWidget'

    def get_other_data(self, question):
        other_data = super().get_other_data(question)
        payment = question.payment or {}
        other_data.update({
            'widget': None,
            'min': 2,
            'max': 15000,
            'is_fixed': bool(payment.get('is_fixed')),
        })
        return other_data

    def get_value(self, question, group_idx):
        return self.get_initial(question)

    def get_tags_data(self, question, group_idx):
        return [{
            'tag': 'input',
            'attrs': {
                'type': 'text',
                'value': self.get_value(question, group_idx) or '0',
            },
        }]


class FieldChoices(Field):
    slug = 'answer_choices'
    data_source = 'survey_question_choice'

    def get_field_data(self, question, group_idx, language):
        field = super().get_field_data(question, group_idx, language)
        field.update({
            'type': 'choices',
            'is_allow_multiple_choice': question.is_allow_multiple_choice,
            'is_disabled_init_item': question.is_disabled_init_item,
            'suggest_choices': question.suggest_choices,
            'widget': question.widget,
            'data_source': self.get_data_source(question, language),
            'value': self.get_value(question, group_idx),
            'show_conditions': self.get_show_conditions(question),
        })
        return field

    def get_other_data(self, question):
        return None

    def get_value(self, question, group_idx):
        value = super().get_value(question, group_idx)
        if value:
            value = [
                self.get_value_item(it)
                for it in value
            ]
        return value

    def _get_value_attr(self, value, attr_name):
        if value:
            if isinstance(value, ChoiceValue):
                return getattr(value, attr_name, None)
            if isinstance(value, dict):
                return value.get(attr_name)

    def get_value_item(self, data):
        return {
            'id': self._get_value_attr(data, 'key'),
            'text': self._get_value_attr(data, 'text'),
        }

    def modify_order(self, question, items):
        if question.modify_choices == 'natural':
            return items
        elif question.modify_choices == 'sort':
            sort_key = lambda it: it['text']
            return sorted(items, key=sort_key)
        elif question.modify_choices == 'shuffle':
            random.shuffle(items)
            return items
        return items

    def get_data_source(self, question, language):
        choices = self.choices.get(question.pk)
        return {
            'items': self.modify_order(question, [
                {
                    'id': str(choice.pk),
                    'slug': choice.slug,
                    'text': self.get_translated_field(choice, 'label', language),
                    'label_image': get_image(choice.image),
                }
                for choice in choices or []
            ])
        }


class FieldMatrix(FieldChoices):
    data_source = 'survey_question_matrix_choice'

    def _get_value_attr(self, value, attr_name):
        if value:
            if isinstance(value, TitleValue):
                return getattr(value.row, attr_name, None), getattr(value.col, attr_name, None)
            if isinstance(value, dict):
                return value.get('row', {}).get(attr_name), value.get('col', {}).get(attr_name)

    def get_value_item(self, data):
        return {
            'id': '%s_%s' % self._get_value_attr(data, 'key'),
            'text': '"%s": %s' % self._get_value_attr(data, 'text'),
        }

    def get_data_source(self, question, language):
        titles = self.titles.get(question.pk)
        return {
            'items': [
                {
                    'id': str(title.pk),
                    'position': title.position,
                    'text': self.get_translated_field(title, 'label', language),
                    'type': title.type,
                }
                for title in titles or []
            ]
        }


class FieldDataSource(FieldChoices):
    data_source = None

    def get_data_source(self, question, language):
        return self._get_data_source(question.data_source, question.data_source_params)


class FieldAgreement(Field):
    slug = 'agreement'

    def as_dict(self, agreement, page, position, language):
        field_name = 'is_agree_with_%s' % agreement.slug
        return {
            'name': field_name,
            'type_slug': self.slug,
            'label': self.get_translated_field(agreement, 'text', language, '?'),
            'is_required': agreement.is_required,
            'page': page,
            'position': position,
            'tags': [{
                'tag': 'input',
                'attrs': {
                    'type': 'checkbox',
                    'name': field_name,
                },
            }],
            'other_data': {
                'widget': 'agreement',
            },
            'is_hidden': False,
            'errors': self.get_errors(),
            'error_messages': self.get_error_messages(),
        }


class FieldCaptcha(Field):
    slug = 'captcha'

    def as_dict(self, page, position):
        captcha_type = self.survey.captcha_type
        image_url, captcha_key = Captcha().generate(type=captcha_type)
        return {
            'name': 'captcha',
            'label': _('enter_number_from_image'),
            'type_slug': self.slug,
            'position': position,
            'page': page,
            'is_required': True,
            'is_hidden': False,
            'other_data': {
                'widget': 'captcha',
                'captcha_type': captcha_type,
            },
            'tags': [
                {
                    'tag': 'input',
                    'attrs': {
                        'type': 'hidden',
                        'data-captcha-image-url': image_url,
                        'captcha_type': captcha_type,
                        'value': captcha_key,
                        'name': 'captcha_0',
                    },
                },
                {
                    'tag': 'input',
                    'attrs': {
                        'type': 'text',
                        'data-captcha-image-url': image_url,
                        'captcha_type': captcha_type,
                        'name': 'captcha_1',
                    },
                },
            ],
            'errors': self.get_errors(),
            'error_messages': self.get_error_messages(),
        }


class FormClass:
    def __init__(self, survey_id, user=None, orgs=None, is_preview=False, captcha_controller=None, survey_key=None):
        self.user = user  # type: User
        self.orgs = orgs or []  # type: List[str]
        self.is_preview = is_preview  # type: bool
        self.survey_key = survey_key  # type: str
        self.captcha_controller = captcha_controller  # type: CaptchaController
        self.errors = {}  # type: Dict[str, str]
        self.last_answer = None  # type: AnswerData

        self.survey = get_survey(survey_id)  # type: SurveyData
        self.texts = []  # type: List[TextData]
        self.agreements = []  # type: List[AgreementData]
        self.questions = {}  # type: Dict[int, QuestionData]
        self.choices = {}  # type: Dict[int, List[ChoiceData]]
        self.titles = {}  # type: Dict[int, List[TitleData]]
        self.show_conditions = {}  # type: Dict[int, Dict[int, ConditionItemData]]
        self.submit_conditions = {}  # type: Dict[int, ConditionItemData]

        self.is_survey_ready = self.check_if_survey_ready()  # type: bool
        if self.is_survey_ready:
            self._init_metadata()

        self.field_types = {
            field.slug: field
            for field in (
                FieldBoolean(self),
                FieldChoices(self),
                FieldDate(self),
                FieldEmail(self),
                FieldFiles(self),
                FieldGroup(self),
                FieldLongText(self),
                FieldNumber(self),
                FieldPayment(self),
                FieldPhone(self),
                FieldShortText(self),
                FieldStatement(self),
                FieldUrl(self),
                FieldName(self),
                FieldSurname(self),
            )
        }

    def _init_metadata(self):
        self.texts = get_texts(self.survey.pk)
        self.agreements = get_agreements(self.survey.pk)
        self.questions = get_questions(self.survey.pk)
        # оптимизация на случай формы без вопросов
        if self.questions:
            self.choices = get_choices(self.survey.pk, self.questions)
            self.titles = get_titles(self.survey.pk, self.questions)
            self.show_conditions = get_show_conditions(self.survey.pk)
            self.submit_conditions = get_submit_conditions(self.survey.pk)

    def get_field_type(self, question):
        slug = question.answer_type_slug
        if slug == 'answer_choices':
            if question.data_source == 'survey_question_matrix_choice':
                return FieldMatrix(self)
            elif question.data_source != 'survey_question_choice':
                return FieldDataSource(self)
        elif slug == 'answer_date':
            if question.date_field_type == 'daterange':
                return FieldDateRange(self)
        return self.field_types[slug]

    def check_if_user_authenticated(self):
        return self.user and self.user.pk != settings.ROBOT_USER_ID and self.user.uid

    def check_if_survey_key_active(self):
        return (
            SurveyKey.objects.using(settings.DATABASE_ROLOCAL)
            .filter(
                bundle__survey_id=self.survey.pk,
                value=self.survey_key,
                is_active=True,
            )
            .exists()
        )

    def check_if_survey_ready(self):
        # это предпросмотр формы
        if self.is_preview:
            return True
        # форма не опубликована
        if not self.survey.is_published_external:
            # не передан ключ или ключ неактивен
            if not self.survey_key or not self.check_if_survey_key_active():
                self.errors['survey_closed'] = 'survey_closed'
                return False
        # для ответа требуется залогин
        if self.survey.need_auth_to_answer:
            # пользователь не залогинен
            if not self.check_if_user_authenticated():
                self.errors['not_logged'] = 'not_logged'
                return False
        # если ответ можно редактировать
        # или требуется показать последний заполненный ответ
        # или необходимо обеспечить чтобы ответ был единственным
        if (
            self.survey.is_allow_answer_editing
            or self.survey.is_allow_answer_versioning
            or (not self.survey.is_allow_answer_editing and not self.survey.is_allow_multiple_answers)
        ):
            if self.check_if_user_authenticated() and self.user.pk:
                self.last_answer = get_last_answer(self.survey.pk, self.user.pk)
            # на форму можно ответить только один раз
            if not self.survey.is_allow_answer_editing and not self.survey.is_allow_multiple_answers:
                # ответ найден
                if self.last_answer:
                    self.errors['already_answered'] = 'already_answered'
                    return False
            if self.last_answer:
                self.last_answer = answer_as_dict(self.last_answer)
        # проверки для б2б
        if settings.IS_BUSINESS_SITE:
            # для супрепользователя форма доступна
            if self.user and self.user.is_superuser:
                return True
            # форма не публичная
            if not self.survey.is_public:
                # форма создана в организации и пользователь из этой организации
                if self.survey.dir_id and self.survey.dir_id in self.orgs:
                    return True
                self.errors['not_public'] = 'not_public'
                return False
        return True

    def get_form_conditions(self, conditions):
        if conditions:
            return [
                [
                    {
                        'field': self.get_question_slug(item.question_id),
                        'condition': item.condition,
                        'operator': item.operator,
                        'field_value': item.value,
                    }
                    for item in items
                ]
                for items in conditions.values()
                if isinstance(items, list)
            ] or None

    def get_form_show_conditions(self, question):
        if self.show_conditions:
            conditions = self.show_conditions.get(question.pk)
            return self.get_form_conditions(conditions)

    def get_form_submit_conditions(self):
        return self.get_form_conditions(self.submit_conditions)

    def get_form_texts(self, language):
        return {
            text.slug: self.get_translated_field(text, 'value', language)
            for text in self.texts
        }

    def get_styles_template(self):
        styles = self.survey.styles
        if styles:
            return {
                'styles': styles,
                'image_page': get_image(self.survey.image_page),
                'image_form': get_image(self.survey.image_form),
            }

    def get_group_slug(self, question):
        if question.group_id:
            group_question = self.questions.get(question.group_id)
            if group_question:
                return group_question.slug

    def get_question_slug(self, question_id):
        question = self.questions.get(question_id)
        if question:
            if question.group_id:
                return '%s__0' % question.slug
            return question.slug

    def get_question_field(self, question, language):
        field = self.get_field_type(question)
        yield from field.as_dict(question, language)

    def get_agreement_field(self, agreement, page, position, language):
        return FieldAgreement(self).as_dict(agreement, page, position, language)

    def get_captcha_field(self, page, position):
        return FieldCaptcha(self).as_dict(page, position)

    def get_last_field(self, fields):
        try:
            return next(reversed(fields.values()))
        except StopIteration:
            return {}

    def check_show_captcha(self):
        if self.survey.captcha_display_mode == 'always' or Survey.get_spam_detected(self.survey.pk):
            return True
        if self.captcha_controller:
            return self.captcha_controller.is_enabled()
        return False

    def get_fields(self, language):
        fields = OrderedDict()
        for question in self.questions.values():
            for field in self.get_question_field(question, language):
                fields[field['name']] = field

        if self.agreements:
            last_field = self.get_last_field(fields)
            page = last_field.get('page', 1)
            position = last_field.get('position', 0) + 1
            for pos, agreement in enumerate(self.agreements or [], start=position):
                field = self.get_agreement_field(agreement, page, pos, language)
                fields[field['name']] = field

        if self.is_survey_ready and self.check_show_captcha():
            last_field = self.get_last_field(fields)
            page = last_field.get('page', 1)
            position = last_field.get('position', 0) + 1
            fields['captcha'] = self.get_captcha_field(page, position)

        return fields

    def exists(self):
        return bool(self.survey)

    def get_translated_field(self, obj, name, language, default_language=None):
        default_language = default_language or self.survey.language
        return get_translated(obj, name, language, default_language)

    def get_survey_group_info(self):
        if self.survey.group_pk:
            return {
                'id': self.survey.group_pk,
                'metrika_counter_code': self.survey.group_metrika_counter_code,
            }

    def as_dict(self, language):
        if self.exists():
            extra = self.survey.extra or {}
            return {
                'id': self.survey.pk,
                'slug': self.survey.slug,
                'group': self.get_survey_group_info(),
                'name': self.get_translated_field(self.survey, 'name', language),
                'metrika_counter_code': self.survey.metrika_counter_code,
                'footer': extra.get('footer'),
                'redirect': extra.get('redirect'),
                'stats': extra.get('stats'),
                'teaser': extra.get('teaser'),
                'is_only_for_iframe': self.survey.is_only_for_iframe,
                'is_answer_could_be_edited': self.survey.is_allow_answer_editing,
                'is_user_already_answered': bool(self.last_answer),
                'org_id': self.survey.dir_id,
                'why_user_cant_answer': self.errors,
                'is_user_can_answer': self.is_survey_ready,
                'styles_template': self.get_styles_template(),
                'texts': self.get_form_texts(language),
                'allow_post_conditions': self.get_form_submit_conditions(),
                'fields': self.get_fields(language),
                'non_field_errors': [],
            }
