from collections import defaultdict
from typing import Dict, Any, List

from django import forms
from django.db import models
from django.db.models import NOT_PROVIDED
from django.utils.translation import ugettext_lazy as _

from idm.core import validators, canonical
from idm.core.constants.rolefield import FIELD_TYPE
from idm.framework.fields import StrictForeignKey, NullJSONField as JSONField
from idm.framework.mixins import LocalizedModel
from idm.utils import reverse
from idm.utils.i18n import get_lang_pair, get_lang_key


class RoleFieldFrontendApiMixin:
    @classmethod
    def get_fields_suggest_registry(cls):
        if cls._suggest_resources_registry is None:
            from idm.api.frontend.suggest.fields.usable_in_fields import _REGISTRY
            cls._suggest_resources_registry = _REGISTRY
        return cls._suggest_resources_registry

    def is_shareable(self):
        return self.type != FIELD_TYPE.PASSPORT_LOGIN

    def as_frontend_api(self):
        lang_code = get_lang_key()
        result = {
            'slug': self.slug,
            'type': self.type,
            'required': self.is_required,
            'name': self.get_localized_field('name'),
            'is_shareable': self.is_shareable(),
        }
        if self.options:
            result['options'] = {k: v for k, v in self.options.items() if k in self.ALLOWED_OPTIONS}

            if self.type == FIELD_TYPE.SUGGEST:
                # заменяем имя саджеста на его url
                suggest_resource = self.get_fields_suggest_registry()[result['options']['suggest']]
                result['options']['suggest'] = reverse(
                    'api_dispatch_list',
                    api_name='frontend',
                    resource_name=suggest_resource.Meta.resource_name,
                )

            if self.type == FIELD_TYPE.CHOICE:
                result['options']['custom'] = bool(self.options.get('custom', False))

            choices = self.options.get('choices')
            if choices is not None:
                result['options']['choices'] = [{
                    'value': item['value'],
                    'name': item['name'][lang_code],
                } for item in choices]

        return result


class RoleField(RoleFieldFrontendApiMixin, models.Model, LocalizedModel):
    IGNORED_USER_FIELD_TYPES = []
    IGNORED_GROUP_FIELD_TYPES = [FIELD_TYPE.PASSPORT_LOGIN]
    VALIDATORS = {
        'sudoers_entry': validators.sudoers_entry,
    }
    ALLOWED_OPTIONS = ('placeholder', 'default', 'widget', 'custom', 'choices', 'suggest')
    created_at = models.DateTimeField(_('Дата создания'), auto_now_add=True, editable=False, null=True)
    updated_at = models.DateTimeField(_('Дата обновления'), auto_now=True, editable=False, null=True)
    removed_at = models.DateTimeField(_('Дата удаления'), editable=False, null=True)

    type = models.CharField(_('Тип поля'), choices=FIELD_TYPE.TYPE_CHOICES, max_length=255,
                            db_index=True, default=FIELD_TYPE.CHARFIELD)
    is_required = models.BooleanField(_('Обязательное поле'), default=False)
    is_active = models.BooleanField(_('Действующее поле'), default=True)
    slug = models.SlugField(_('Slug поля'), max_length=255, default='')
    name = models.CharField(_('Название поля'), max_length=255, default='', blank=True)
    name_en = models.CharField(_('Название поля (по-английски)'), max_length=255, default='', blank=True)
    options = JSONField(_('Настройки поля'), null=True, blank=True)
    dependencies = JSONField(_('Зависимости от других полей'), null=True, blank=True)
    node = StrictForeignKey(
        'core.RoleNode',
        null=False, blank=False,
        verbose_name=_('Узел дерева ролей'),
        related_name='fields',
        on_delete=models.CASCADE,
    )

    class Meta:
        verbose_name = _('Поле узла дерева ролей')
        verbose_name_plural = _('Поля узлов дерева ролей')
        db_table = 'upravlyator_rolefield'

    def __str__(self):
        return '%s[*:%s]: %s' % (self.get_type_display(), 'Y' if self.is_required else 'N', self.name)

    _suggest_resources_registry = None

    @classmethod
    def check_options(cls, field_type: str, options: Dict[str, Any]):
        suggest = None
        if isinstance(options, dict):
            if 'validators' in options:
                if not isinstance(options['validators'], (list, tuple)):
                    raise ValueError(_('Валидаторы поля должны быть списком строк'))
                for validator_name in options['validators']:
                    if validator_name not in cls.VALIDATORS:
                        raise ValueError(_('Валидатор <%s> неизвестен' % validator_name))

            suggest = options.get('suggest')
            choices = options.get('choices')
            if choices is not None:
                options['choices'] = cls.prepare_choices(choices)
        elif options is not None:
            raise ValueError(_('Опции поля должны быть словарем'))

        if suggest is not None:
            registry = cls.get_fields_suggest_registry()
            if suggest not in registry:
                raise ValueError(_('Неизвестный suggest: ') + str(suggest))
        else:
            if field_type == FIELD_TYPE.SUGGEST:
                raise ValueError(_('Опция suggest является обязательной для suggestfield'))

    @classmethod
    def prepare_choices(cls, choices: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
        new_choices = []
        for namevalue in choices:
            name = namevalue.get('name', None)
            value = namevalue.get('value', None)
            if name is None or value is None:
                continue
            name_ru, name_en = get_lang_pair(name)
            new_choices.append({
                'value': value,
                'name': {
                    'ru': name_ru,
                    'en': name_en,
                }
            })
        return new_choices

    def save(self, *args, **kwargs):
        self.__class__.check_options(self.type, self.options)
        return super(RoleField, self).save(*args, **kwargs)

    def as_canonical(self):
        options = self.options
        if options == '':
            options = None
        dependencies = self.dependencies
        if dependencies == '':
            dependencies = None
        return canonical.CanonicalField(
            slug=self.slug,
            type=self.type,
            is_required=self.is_required,
            name=self.name,
            name_en=self.name_en,
            options=options,
            dependencies=dependencies,
        )

    @classmethod
    def from_canonical(cls, canonical):
        return cls(
            slug=canonical.slug,
            type=canonical.type,
            is_required=canonical.is_required,
            name=canonical.name,
            name_en=canonical.name_en,
            options=canonical.options,
            dependencies=canonical.dependencies,
        )

    def natural_key(self):
        return self.node.natural_key(), self.slug

    def as_api(self):
        result = {
            'type': self.type,
            'required': self.is_required,
            'slug': self.slug,
            'name': {
                'ru': self.name,
                'en': self.name_en,
            },
        }
        if self.options:
            result['options'] = self.options
        if self.dependencies:
            result['depends_on'] = self.dependencies
        return result

    def as_formfield(self):
        from idm.core import fields as idm_fields
        formfield_classes = {
            FIELD_TYPE.CHARFIELD: idm_fields.ApiCharField,
            FIELD_TYPE.BOOLEAN: forms.BooleanField,
            FIELD_TYPE.INTEGER: forms.IntegerField,
            FIELD_TYPE.PASSPORT_LOGIN: idm_fields.ApiCharFieldDefaultNull,
            FIELD_TYPE.CHOICE: idm_fields.ApiChoiceField,
            FIELD_TYPE.SUGGEST: idm_fields.ApiSuggestField,
        }
        form_class = formfield_classes[self.type]
        options = {
            k: v for k, v in list((self.options or {}).items())
            if k in ('choices', 'custom', 'required', 'validators', 'widget', 'blank_allowed', 'suggest')
        }
        if 'validators' in options:
            options['validators'] = [
                self.VALIDATORS[validator_name]
                for validator_name in options['validators']
            ]
        return form_class(required=self.is_required, **options)

    def get_name(self, lang=None):
        return self.get_localized_field('name', lang)

    def get_dependent_fieldnames(self):
        def inner_search(dict_):
            local_deps = {key for key in dict_.keys() if not key.startswith('$')}
            substructures = [subdict for key, subdict in dict_.items() if key.startswith('$')]
            for substructure in substructures:
                if isinstance(substructure, dict):
                    local_deps |= inner_search(substructure)
                elif isinstance(substructure, list):
                    for item in substructure:
                        local_deps |= inner_search(item)
            return local_deps

        deps = set()
        if self.dependencies:
            deps = inner_search(self.dependencies)
        return deps

    def check_dependencies(self, fields_data):
        mapping = {
            '$exists': lambda value, parameter: value is not NOT_PROVIDED if parameter else value is NOT_PROVIDED,
            '$in': lambda value, parameter: value in parameter,
            '$nin': lambda value, parameter: value not in parameter,
            '$eq': lambda value, parameter: value == parameter,
            '$gt': lambda value, parameter: value > parameter,
            '$gte': lambda value, parameter: value >= parameter,
            '$lt': lambda value, parameter: value < parameter,
            '$lte': lambda value, parameter: value <= parameter,
            '$ne': lambda value, parameter: value != parameter,
        }

        def check_field(condition, got_value):
            if isinstance(condition, dict):
                assert len(condition) == 1  # пока поддерживаем только простые условия
                key, parameter = list(condition.items())[0]
                return mapping[key](got_value, parameter)
            else:
                return condition == got_value

        def check_deps(dependencies):
            results = []
            for key, value in dependencies.items():
                if not key.startswith('$'):
                    result = check_field(value, fields_data.get(key, NOT_PROVIDED))
                    results.append(result)
                elif key == '$or':
                    results.append(any((check_deps(item) for item in value)))
            return all(results)

        if not self.dependencies:
            return True

        return check_deps(self.dependencies)

    def get_value_name_map(self) -> Dict[str, Any]:
        name_map = {}
        options = (self.options or {})
        if not options.get('display_name', False):
            return name_map
        for option in options.get('choices', []):
            value = option.get('value')
            name = option.get('name')
            if name is None:
                continue
            elif isinstance(name, dict):
                name = name.get(get_lang_key())
            name_map[value] = name
        return name_map
