import re
from collections import OrderedDict

from django.conf import settings
from django.core.exceptions import ValidationError
from django.utils.functional import Promise

from orchestra.core import validators
from orchestra.utils.python import import_class, format_exception


default_app_config = 'orchestra.contrib.settings.apps.SettingsConfig'


class Setting(object):
    """
    Keeps track of the defined settings and provides extra batteries like value validation.
    """
    conf_settings = settings
    settings = OrderedDict()
    
    def __str__(self):
        return self.name
    
    def __repr__(self):
        value = str(self.value)
        value = ("'%s'" if isinstance(value, str) else '%s') % value
        return '<%s: %s>' % (self.name, value)
    
    def __new__(cls, name, default, help_text="", choices=None, editable=True, serializable=True,
                multiple=False, validators=[], types=[], call_init=False):
        if call_init:
            return super(Setting, cls).__new__(cls)
        cls.settings[name] = cls(name, default, help_text=help_text, choices=choices, editable=editable,
            serializable=serializable, multiple=multiple, validators=validators, types=types, call_init=True)
        return cls.get_value(name, default)
    
    def __init__(self, *args, **kwargs):
        self.name, self.default = args
        for name, value in kwargs.items():
            setattr(self, name, value)
        self.value = self.get_value(self.name, self.default)
        self.settings[name] = self
    
    @classmethod
    def validate_choices(cls, value):
        if not isinstance(value, (list, tuple)):
            raise ValidationError("%s is not a valid choices." % value)
        for choice in value:
            if not isinstance(choice, (list, tuple)) or len(choice) != 2:
                raise ValidationError("%s is not a valid choice." % choice)
            value, verbose = choice
            if not isinstance(verbose, (str, Promise)):
                raise ValidationError("%s is not a valid verbose name." % value)
    
    @classmethod
    def validate_import_class(cls, value):
        try:
            import_class(value)
        except Exception as exc:
            raise ValidationError(format_exception(exc))
    
    @classmethod
    def validate_model_label(cls, value):
        from django.apps import apps
        try:
            apps.get_model(*value.split('.'))
        except Exception as exc:
            raise ValidationError(format_exception(exc))
    
    @classmethod
    def string_format_validator(cls, names, modulo=True):
        def validate_string_format(value, names=names, modulo=modulo):
            errors = []
            regex = r'%\(([^\)]+)\)' if modulo else r'{([^}]+)}'
            for n in re.findall(regex, value):
                if n not in names:
                    errors.append(
                        ValidationError('%s is not a valid format name.' % n)
                    )
            if errors:
                raise ValidationError(errors)
        return validate_string_format
    
    def validate_value(self, value):
        if value:
            validators.all_valid(value, self.validators)
        valid_types = list(self.types)
        if self.choices:
            choices = self.choices
            if callable(choices):
                choices = choices()
            choices = [n for n,v in choices]
            values = value
            if not isinstance(values, (list, tuple)):
                values = [value]
            for cvalue in values:
                if cvalue not in choices:
                    raise ValidationError("'%s' not in '%s'" % (value, ', '.join(choices)))
        if isinstance(self.default, (list, tuple)):
            valid_types.extend([list, tuple])
        valid_types.append(type(self.default))
        if not isinstance(value, tuple(valid_types)):
            raise ValidationError("%s is not a valid type (%s)." %
                (type(value).__name__, ', '.join(t.__name__ for t in valid_types))
            )
    
    def validate(self):
        self.validate_value(self.value)
    
    @classmethod
    def get_value(cls, name, default):
        return getattr(cls.conf_settings, name, default)