from django.forms import ValidationError
from django.forms.formsets import DELETION_FIELD_NAME
from django.forms.models import BaseInlineFormSet, inlineformset_factory, ModelForm
from django.utils.translation import ngettext, ugettext_lazy as _

from mtp.apps.common.forms_utils import OrderableInlineFormset
from .models import Choice, Question, SurveyPool, Survey


class ChoicesInlineFormset(OrderableInlineFormset):

    def add_fields(self, form, index):
        super().add_fields(form, index)
        form.fields['value_text'].label = _("Value")

        # To avoid django raising a Validation error on non-filled label on forms marked as
        # deleted, we mark this field as non-required and will do the check ourselves in the
        # ``clean`` method.
        form.fields['label'].required = False


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

        # For readability.
        question = self.instance

        if not Question.CHOICES_EXPECTED[question.question_type]:
            # Don't do validation on choices if choices are not expected.
            return

        # We have a question that expects choices, we must validate them.

        # Check that each form has a valid value.
        for form in self.forms:

            # Ignore forms where all fields are empty (the form will be ignored).
            if not form.cleaned_data:
                continue

            # Ignore choices marked as deleted.
            if form.cleaned_data[DELETION_FIELD_NAME]:
                pass

            # Check that the label is defined.
            label = form.cleaned_data.get('label').strip()
            if not label:
                form.add_error('label', ValidationError(
                    _('A label is required.'),
                    code='label_expected'
                ))

            # Get the entered value, and strip it.
            value = form.cleaned_data.get('value_text')
            if value is not None:
                form.cleaned_data['value_text'] = value = value.strip()

            # Validate that the field is not filled if there is no value type.
            if not question.value_type:
                if value:
                    form.add_error('value_text', ValidationError(
                        _('No value expected.'),
                        code='no_value_expected',
                    ))
                else:
                    form.instance.value_text = ""
                    form.instance.value_number = None

                continue

            # Validate that a field is filled if there is a value_type.
            if not value:
                form.add_error('value_text', ValidationError(
                    _('A value is expected.'),
                    code='value_expected',
                ))
                continue

            # Validate that the value is an integer if the question expect an integer.
            if question.value_type in (Question.VALUE_TYPES.PERCENT, Question.VALUE_TYPES.NUMBER):
                field = form.instance._meta.get_field('value_number')
                try:
                    field.clean(value, form.instance)
                except ValidationError as e:
                    form.add_error('value_text', ValidationError(
                        _('A number is expected.'),
                        code='number_expected',
                    ))
                    continue

                int_value = int(value)

                if question.value_type == Question.VALUE_TYPES.PERCENT:
                    if int_value < 0 or int_value > 100:
                        form.add_error('value_text', ValidationError(
                            _('A percentage should be comprised between 0 and 100.'),
                            code='percent_expected',
                        ))
                        continue

                form.instance.value_number = int_value
                # ``value_text`` is always filled with the last saved value, even for numbers
                # because we use this field to display the form.
                form.instance.value_text = value

            # In the other case we expect a text, so it's valid.
            else:
                form.instance.value_text = value
                form.instance.value_number = None

        # Check the number of filled forms, excluding deleted choices.
        nb_forms = len([f for f in self.forms if f.cleaned_data
                        and not f.cleaned_data.get(DELETION_FIELD_NAME)])

        if nb_forms < Question.MIN_CHOICES:
            raise ValidationError(
                ngettext(
                    'You must enter at least %(min)s choice.',
                    'You must enter at least %(min)s choices.',
                    Question.MIN_CHOICES
                ) % {'min': Question.MIN_CHOICES},
                code='not_enough_forms'
            )


ChoicesFormSet = inlineformset_factory(
    Question,
    Choice,
    fields=['label', 'value_text'],
    formset=ChoicesInlineFormset,
    extra=0, # Be sure to have all forms displayed even if no choices exist.
    min_num=0,   # Allow no choices for open questions.
    max_num=Question.MAX_CHOICES,  # And we don't want more than the defined max..
    validate_min=False,  # We'll manage the min number of choices ourselves depening on the
                         # question type.
    validate_max=True,  # Let django manage the max number of choices.
    can_delete=True,  # Default value but explicit is better than implicit,
    can_order=True,  # Default to False but here we want ordering
)


class QuestionForm(ModelForm):
    class Meta:
        model = Question
        fields = ['title', 'question_type', 'value_type', 'is_for_global_satisfaction']

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

        question_type = cleaned_data.get('question_type')
        value_type = cleaned_data.get('value_type')

        # Check that the ``value_type`` field matches the ``question_type`` one, if both are
        # defined, using the ``ALLOWED_VALUE_TYPES`` dict.
        if question_type and \
                value_type not in Question.ALLOWED_VALUE_TYPES[question_type]:

            allowed_types = [
                '"%s"' % Question.VALUE_TYPES.for_value(val).display
                for val in Question.ALLOWED_VALUE_TYPES[question_type]
            ]

            # We generate a message with allowed value types.
            msg = ngettext(
                ('For the selected question type ("%(question_type)s"), '
                'only the following choice is allowed: %(value_types)s'),
                ('For the selected question type ("%(question_type)s"), '
                'only the following choices are allowed: %(value_types)s'),
                 len(allowed_types)
            ) % {
                'question_type': Question.QUESTION_TYPES.for_value(question_type).display,
                'value_types': ', '.join(allowed_types),
            }

            self.add_error('value_type', ValidationError(msg, code='bad_value_type'))

        if question_type and cleaned_data.get('is_for_global_satisfaction') and not (
                question_type == Question.QUESTION_TYPES.SINGLE_CHOICE and
                value_type == Question.VALUE_TYPES.PERCENT
            ):
            raise ValidationError(
                _("You can only use a question for global satisfaction if it's a single choice "
                  "question with percentage."),
                code='global_satisfaction_unexpected'
            )

        # Always return cleaned data, even if not updated.
        return cleaned_data


class SurveyPoolsInlineFormset(BaseInlineFormSet):

    def save(self, commit=True):
        super().save(commit)

        updated_forms = set()
        for form in self.forms:

            # Now that the formset is saved, we can remove the ``is_new`` flag of all pools.
            if form.instance.is_new:
                form.instance.is_new = False
                updated_forms.add(form)

            # And enforce the ``is_displayed`` on mandatory questions.
            if not form.instance.is_displayed and form.instance.pool.is_mandatory:
                form.instance.is_displayed = True
                updated_forms.add(form)

        # Finally save all forms we just updated.
        for form in updated_forms:
            form.save()


SurveyPoolsFormSet = inlineformset_factory(
    Survey,
    SurveyPool,
    fields=['is_displayed'],
    formset=SurveyPoolsInlineFormset,
    extra=0,  # We don't want the trainer to add new entries.
    can_delete=False,  # We don't want the trainer to remove entries.
)
