from rest_framework import serializers

from question.models import QUESTION_TYPE_SERIALIZER_CHOICES, QUESTION_FORMAT_SERIALIZER_CHOICES, QUESTION_TYPE_MULTIPLE_CHOICE, \
    QUESTION_TYPE_DESCRIPTIVE
from question.models import Question
from quiz.blueprint import const
from utils.validators import dic_validetor, uuid_checker


class Rule:
    def __init__(self, retrievers, groups):
        self.retrievers = retrievers
        self.groups = groups

    @classmethod
    def from_dict(cls, input):
        if cls.dict_validator(input):
            retrievers = [RuleRetriever.from_dict(retriever) for retriever in input['retrievers']]
            groups = [RuleGroup.from_dict(group) for group in input['groups']]
            return cls(retrievers, groups)
        else:
            return None

    @classmethod
    def dict_validator(cls, data) -> bool:
        # check if rule is not a dictionary
        if type(data) is not dict:
            raise serializers.ValidationError(
                {'rule': 'rule should be a json object'})

        # check for required and optional fields
        retrievers = {
            'key': 'retrievers',
            'type': list,
            'validator': lambda retrievers: len(retrievers) > 0,
            'validator_error_message': 'At least one retriever should be added'
        }
        groups = {
            'key': 'groups',
            'type': list,
        }
        required_items = [retrievers, groups]

        dic_validetor(data, required_items=required_items, error_key='rule',
                      many=False, container_name='rule', raise_error_for_unexpected_key=True)

        for retriever in data['retrievers']:
            RuleRetriever.dict_validator(retriever, many=False)
        for group in data['groups']:
            RuleGroup.dict_validator(group, many=False)

        if len(data['groups']) > 0:
            if data['groups'][0]['start_at'] != 0:
                raise serializers.ValidationError(
                    {'rule': "If groups is not empty, the first group should have start_at=0"})

        last_start_at = -1
        for group in data['groups']:
            if group['start_at'] < last_start_at:
                raise serializers.ValidationError({'rule': "group start_at values should be ascending"})
            last_start_at = group['start_at']

        group_names = []
        for group in data['groups']:
            if group['name'] in group_names:
                raise serializers.ValidationError(
                    {'rule': f'Duplicate group name "{group["name"]}"'})
            group_names.append(group['name'])

        return True

    def permission_checker(self, context_institue):
        for retriever in self.retrievers:
            retriever.permission_checker(context_institue)
        return True


class RuleRetriever:
    def __init__(self, insert_type, duration, negative_score, positive_score, exact_question_data, question_bank_id, question_id, count,
                 tag_filters):
        self.insert_type = insert_type
        self.duration = duration
        self.negative_score = negative_score
        self.positive_score = positive_score
        self.exact_question_data = exact_question_data
        self.question_bank_id = question_bank_id
        self.question_id = question_id
        self.count = count
        self.tag_filters = tag_filters

    def permission_checker(self, context_institue):
        if self.insert_type == const.QUIZ_RULE_EXACT_QUESTION:
            # no permission check required
            pass
        elif self.insert_type == const.QUIZ_RULE_REFERENCED_QUESTION:
            # question availability check
            try:
                question = Question.objects.get(id=self.question_id)
            except:
                raise serializers.ValidationError(
                    {'rule': f'Question "{self.question_id}" not found'})
            question_bank = question.question_bank
            # question bank availability check
            if not question_bank:
                raise serializers.ValidationError(
                    {'rule': f'{self.question_id} is not available for now!'})
            elif not context_institue.can_read_question_bank(question_bank):
                raise serializers.ValidationError(
                    {'rule': f'Sorry! you do not have access to question bank "{question_bank.name}" (question_id: {self.question_id})'})

    @classmethod
    def from_dict(cls, input):
        return cls(
            insert_type=input.get("insert_type"),
            duration=input.get("duration"),
            negative_score=input.get("negative_score"),
            positive_score=input.get("positive_score"),
            exact_question_data=input.get("data"),
            question_bank_id=input.get("question_bank_id"),
            question_id=input.get("question_id"),
            count=input.get("count"),
            tag_filters=input.get("tag_filters"),
        )

    @classmethod
    def _input_rule_validator(cls, rule, question_type):

        if question_type == QUESTION_TYPE_DESCRIPTIVE:

            max_characters = {
                'key': 'max_characters',
                'type': int,
            }

            attachment = {
                'key': 'attachment',
                'type': bool,

            }

            required_items = [
                max_characters, attachment]

            dic_validetor(data=rule, required_items=required_items,
                          error_key='input_rule', many=False, container_name='input_rule', raise_error_for_unexpected_key=True)

        elif question_type == QUESTION_TYPE_MULTIPLE_CHOICE:

            max_selectable_choices = {
                'key': 'max_selectable_choices',
                'type': int,
                'validator': lambda max_selectable_choices: 1 <= max_selectable_choices,
                'validator_error_message': 'max_selectable_choices should be an integer between 1 and number of choices'
            }

            required_items = [max_selectable_choices]

            dic_validetor(data=rule, required_items=required_items,
                          error_key='input_rule', many=False, container_name='input_rule', raise_error_for_unexpected_key=True)

        else:
            raise serializers.ValidationError(
                {'input_rule': 'bad state'})
        return True

    @classmethod
    def _didicated_referenced_question_dict_validator(cls, data):
        # we already checked if the question is dict
        # check for required fields

        question_id = {
            'key': 'question_id',
            'type': str,
            'validator': lambda question_id: uuid_checker(question_id),
            'validator_error_message': f'{data.get("question_id")} is not a valid UUID.'
        }

        required_items = [question_id]

        dic_validetor(data, required_items=required_items, error_key='rule',
                      many=True, container_name=f'retrievers(with insert_type={const.QUIZ_RULE_REFERENCED_QUESTION})',
                      raise_error_for_unexpected_key=False)

    @classmethod
    def _didicated_exact_question_validator(cls, data):
        data_field = {
            'key': 'data',
            'type': dict,
        }

        dic_validetor(data, required_items=[data_field], error_key='rule', many=True,
                      container_name=f'retrievers(with insert_type={const.QUIZ_RULE_EXACT_QUESTION})',
                      raise_error_for_unexpected_key=False)

        # question dict structure check
        text = {
            'key': 'question_text',
            'type': str,
        }

        format = {
            'key': 'format',
            'type': str,
            'validator': lambda f: f in QUESTION_FORMAT_SERIALIZER_CHOICES,
            'validator_error_message': f'format should be one of {", ".join(QUESTION_FORMAT_SERIALIZER_CHOICES)}'
        }

        question_type = {
            'key': 'question_type',
            'type': str,
            'validator': lambda t: t in QUESTION_TYPE_SERIALIZER_CHOICES,
            'validator_error_message': f'question_type should be one of {", ".join(QUESTION_TYPE_SERIALIZER_CHOICES)}'
        }

        input_rule = {
            'key': 'input_rule',
            'type': dict,
            'validator': lambda ir: RuleRetriever._input_rule_validator(ir, data['data']['question_type']),
        }

        answer_sheet = {
            'key': 'solution',
            'type': str,

        }

        choices = {
            'key': 'choices',
            'type': dict,
        }

        correct_choices = {
            'key': 'correct_choices',
            'type': list,
            'validator': lambda items: len(items) > 0,
            'validator_error_message': f'correct_choices can not be empty'
        }

        required_items = [text, question_type, format, input_rule]
        optional_items = [answer_sheet]
        if data.get('question_type') == QUESTION_TYPE_MULTIPLE_CHOICE:
            required_items += [choices, correct_choices]

        dic_validetor(data['data'], required_items=required_items, optional_items=optional_items,
                      error_key='rule', many=True, container_name='retrievers.data', raise_error_for_unexpected_key=False)

        # input rule dict structure check
        RuleRetriever._input_rule_validator(rule=data.get('data').get('input_rule'),
                                            question_type=data.get('data').get('question_type'))

    @classmethod
    def _tag_filters_validator(cls, tag_filters):
        if len(tag_filters) == 0:
            return
        msg = {
            'rule': 'tag_filters should be a list of lists(ex. [[{"key":"a", "value":"b"}],[{"key":"c", "value":"d"}]])'}
        if type(tag_filters) is not list:
            raise serializers.ValidationError(
                msg)
        for tag_filter in tag_filters:
            if type(tag_filter) is not list:
                raise serializers.ValidationError(msg)
            key = {
                'key': 'key',
                'type': str
            }
            value = {
                'key': 'value',
                'type': str
            }
            required_items = [key, value]

            for item in tag_filter:
                dic_validetor(data=item, required_items=required_items, error_key='rule',
                              many=True, container_name='tag_filters', raise_error_for_unexpected_key=True)

    @classmethod
    def _didicated_query_question_validator(cls, data):
        # we already checked if the question is dict
        # check for required fields

        question_bank_id = {
            'key': 'question_bank_id',
            'type': str,
            'validator': lambda question_bank_id: uuid_checker(question_bank_id),
            'validator_error_message': f'"{data.get("question_bank_id")}" is not a valid UUID.'
        }

        tag_filters = {
            'key': 'tag_filters',
            'type': list
        }

        count = {
            'key': 'count',
            'type': int,
            'validator': lambda c: 1 <= c <= 200,
            'validator_error_message': 'count should be integer and should be between 1, 200'
        }

        type_filter = {
            'key': 'type_filter',
            'type': str,
            'validator': lambda type_filter: type_filter in QUESTION_TYPE_SERIALIZER_CHOICES,
            'validator_error_message': f'In each retrievers, type_filter should be one of {", ".join(QUESTION_TYPE_SERIALIZER_CHOICES)}'
        }

        required_items = [question_bank_id, tag_filters, count]
        optional_items = [type_filter]
        dic_validetor(data, required_items=required_items, optional_items=optional_items,
                      error_key='rule', many=True, container_name='retrievers(with insert_type=QUERY_QUESTION)',
                      raise_error_for_unexpected_key=False)

        RuleRetriever._tag_filters_validator(data['tag_filters'])

    @classmethod
    def dict_validator(cls, data, many):

        if type(data) is not dict:
            raise serializers.ValidationError(
                {'rule': 'each "retriever" should be a valid json object'})

        insert_type = {
            'key': 'insert_type',
            'type': str,
            'validator': lambda t: t in const.QUIZ_RULE_INSERT_TYPES,
            'validator_error_message': f'in each retriever, insert_type should be one of {", ".join(const.QUIZ_RULE_INSERT_TYPES)}',
        }

        positive_score = {
            'key': 'positive_score',
            'type': float
        }

        negative_score = {
            'key': 'negative_score',
            'type': float
        }

        duration = {
            'key': 'duration',
            'type': float,
            'validator': lambda duration: duration > 5.0,
            'validator_error_message': 'in each retriever, duration should be a float number and greater than 5'
        }

        dic_validetor(data, required_items=[insert_type, positive_score, negative_score, duration],
                      error_key='rule', many=many, container_name='retrievers', raise_error_for_unexpected_key=False)

        if data['insert_type'] == const.QUIZ_RULE_REFERENCED_QUESTION:
            RuleRetriever._didicated_referenced_question_dict_validator(
                data)
        elif data['insert_type'] == const.QUIZ_RULE_QUERY_QUESTION:
            RuleRetriever._didicated_query_question_validator(data)
        elif data['insert_type'] == const.QUIZ_RULE_EXACT_QUESTION:
            RuleRetriever._didicated_exact_question_validator(data)

        return True


class RuleGroup:
    def __init__(self, name, start_at):
        self.name = name
        self.start_at = start_at

    @classmethod
    def from_dict(cls, input):
        return cls(
            name=input.get("name"),
            start_at=input.get("start_at"),
        )

    @classmethod
    def dict_validator(cls, data, many):
        if type(data) is not dict:
            raise serializers.ValidationError(
                {'rule': 'each "group" should be a valid json object'})

        name = {
            'key': 'name',
            'type': str,
            'validator': lambda name: len(name) > 3 and len(name) < 1000,
            'validator_error_message': 'In each group, name should be at most 1000 characters'
        }

        start_at = {
            'key': 'start_at',
            'type': int,
            'validator_error_message': f'in each group, start_at should be an integer.',
        }

        dic_validetor(data, required_items=[name, start_at],
                      error_key='rule', many=many, container_name='groups', raise_error_for_unexpected_key=False)

        return True
