import functools
import inspect
from collections import OrderedDict
import random

from django.core.exceptions import ImproperlyConfigured, FieldDoesNotExist
from django.db.models.fields import CharField, TextField
from django.http.request import QueryDict
from django import forms
try:
    from django.utils.encoding import force_text
except ImportError:
    from django.utils.encoding import force_str as force_text
from djingles.formatters import Formatter
from djingles.forms.fields import SortField
from djingles import utils


__all__ = ['ActionFormMixin', 'ActionModelForm', 'ActionForm', 'FilterForm',
           'FilterModelForm', 'FilterFormMixin', 'action_model_form_factory']


def _find_decorated_methods(form, attribute):
    for name, func in inspect.getmembers(form, inspect.ismethod):
        if name.startswith("_"):
            continue
        if hasattr(func, attribute):
            yield func


class ActionFormMixin(object):

    confirmation_message = None
    failure_message = None
    success_message = None

    def __init__(self, **kwargs):
        self.context = kwargs.pop("context", {})
        self.action = self.context.get("action", "execute")
        self.context["action"] = self.action
        self.action_method = getattr(self, self.action, self.execute)
        super(ActionFormMixin, self).__init__(**kwargs)
        prepare = getattr(self, 'prepare_%s' % self.action, None)
        if prepare is not None:
            prepare()

    def prepare_if_exists(self, func):
        spec = inspect.getfullargspec(func)
        names = [n for n in spec.args if n != "self"]
        try:
            args = [self.fields[f] for f in names]
        except KeyError:
            pass
        else:
            return func(*args)

    def clean_if_exists(self, func):
        spec = inspect.getfullargspec(func)
        names = [n for n in spec.args if n != "self"]
        try:
            args = [self.cleaned_data[f] for f in names]
            if any(a is None for a in args):
                return
        except KeyError:
            pass
        else:
            return func(*args)

    def __bind_fields(self):
        key = "__bound_with_form"
        for name, field in self.fields.items():
            if hasattr(field, 'bind_form') and not getattr(field, key, False):
                setattr(field, key, True)
                field.bind_form(self)

    def process_context(self, func):
        context = self.context.copy()
        if hasattr(self, "cleaned_data") and hasattr(self, "save"):
            instance = self.save(commit=False)
            context["instance"] = instance
        context["data"] = self.cleaned_data
        spec = inspect.getfullargspec(func)
        if spec.varargs:
            raise ImproperlyConfigured("Form action cannot have variable arguments")
        if spec.varkw:
            return context
        return {k: context[k] for k in spec.args[1:] if k in context}

    def process_result(self, result):
        return result

    @property
    def result(self):
        self.is_valid()
        return self.__result

    def get_success_message(self):
        if self.success_message:
            return self.success_message.format(**self.context)
        return self.success_message

    def get_failure_message(self):
        if self.failure_message:
            return self.failure_message.format(**self.context)
        return self.failure_message

    def get_confirmation_message(self):
        if self.confirmation_message:
            return self.confirmation_message.format(**self.context)
        return self.confirmation_message

    @classmethod
    def class_oid(cls):
        """
        Obfuscated class id
        :return: str
        """
        return utils.create_hash(utils.qualified_name(cls))

    @classmethod
    def call(cls, *kwargs):
        form_obj = cls(**kwargs)
        if not form_obj.is_valid():
            return False, form_obj.errors
        else:
            return True, form_obj.result

    def full_clean(self):
        result = None
        self.__bind_fields()
        super(ActionFormMixin, self).full_clean()
        try:
            _ = self.__result
        except AttributeError:
            try:
                if self.is_bound and not self._errors:
                    method = self.action_method
                    context = self.process_context(method)
                    result = method(**context)
                    result = self.process_result(result)
            except forms.ValidationError as ex:
                self.add_error(None, ex)
            finally:
                self.__result = result

    def select_fields(self, exclude=None, include=None, **extras):
        if exclude is not None:
            for f in exclude:
                self.fields.pop(f, None)
        if include is not None:
            include = set(include)
            for f in list(self.fields.keys()):
                if f not in include:
                    self.fields.pop(f)
        if extras:
            for f in extras:
                self.fields[f] = extras[f]

    def execute(self):
        return {}


class ActionForm(ActionFormMixin, forms.Form):
    pass


class ActionModelForm(ActionFormMixin, forms.ModelForm):

    confirmation_message = "Please confirm that you really wish to {action} this object ?"

    def prepare_delete(self):
        self.select_fields(include=())

    def delete(self):
        self.instance.delete()

    def update(self):
        return self.save()

    def create(self):
        return self.save()

    def clone(self):
        self.instance.id = None
        return self.save()


class FilterFormMixin:

    submit_key = None

    def __init__(self, data=None, *args, **kwargs):
        self.context = kwargs.pop("context", {})
        self.boolean_empty_label = kwargs.pop("boolean_empty_label", "Any")
        self.empty_label = kwargs.pop("empty_label", "--------------")
        unbound = False
        if not data or (self.submit_key and self.submit_key not in data):
            data = None
        super(FilterFormMixin, self).__init__(data=data, *args, **kwargs)
        if self.submit_key:
            self.fields[self.submit_key] = forms.IntegerField(
                widget=forms.HiddenInput,
                required=False
            )
        for key, f in self.fields.items():
            if f.disabled:
                continue
            if isinstance(f, forms.BooleanField) and not isinstance(f, forms.NullBooleanField):
                field = forms.NullBooleanField(required=False,
                                          label=f.label, help_text=f.help_text)
                self.fields[key] = field
                choices=((1, self.boolean_empty_label),(2, "Yes"), (3, "No"))
                field.widget.choices = choices
            if f.required:
                f.required = False
                if hasattr(f, "choices"):
                    choices = list(f.choices)
                    if choices and choices[0][0]:
                        choices.insert(0, ("", self.empty_label))
                    f.choices = tuple(choices)

    def __bind_fields(self):
        key = "__bound_with_form"
        for name, field in self.fields.items():
            if hasattr(field, 'bind_form') and not getattr(field, key, False):
                setattr(field, key, True)
                field.bind_form(self)

    @property
    def sort_field(self):
        for name, f in self.fields.items():
            if isinstance(f, SortField):
                return self[name]

    def generic_filter(self, name, queryset, value, data):
        kwargs = {}
        if isinstance(value, (tuple, list)):
            name = '%s__in' % name
        kwargs[name] = value
        return queryset.filter(**kwargs)

    def perform_filter(self, queryset):
        self.__bind_fields()
        if not self.is_bound:
            data = {k: self.get_initial_for_field(f, k) for k, f in self.fields.items()}
        else:
            if not self.is_valid():
                return self.invalid_queryset(queryset)
            data = self.cleaned_data
        allowed = set(self.fields.keys())
        if hasattr(self, "before_filters"):
            queryset = self.before_filters(queryset, data)
        for name, value in data.items():
            if name not in allowed or value in (None, '', [], ()):
                continue
            field = self.fields[name]
            if hasattr(self, "filter_%s" % name):
                result = getattr(self, "filter_%s" % name)(queryset, value, data)
            elif hasattr(field, "filter_queryset"):
                result = field.filter_queryset(queryset, value, self[name])
            else:
                result = self.generic_filter(name, queryset, value, data)
            if result is not None:
                queryset = result
        if hasattr(self, "after_filters"):
            queryset = self.after_filters(queryset, data)
        return queryset

    def invalid_queryset(self, queryset):
        return queryset.none()


class FilterForm(FilterFormMixin, forms.Form):
    pass


class FilterModelForm(FilterFormMixin, forms.ModelForm):

    def generic_filter(self, name, queryset, value, data):
        meta = queryset.model._meta
        try:
            field = meta.get_field(name)
        except FieldDoesNotExist:
            return super(FilterModelForm, self).generic_filter(name, queryset, value, data)
        else:
            op = ""
            if isinstance(value, (list, dict)):
                op = "in"
            elif isinstance(field, (CharField, TextField)):
                op = "icontains"
        query = "%s__%s" % (name, op) if op else name
        kwargs = {query: value}
        return queryset.filter(**kwargs)


def action_model_form_factory(model_class, include=None, exclude=(), **kwargs):
    name = "%sActionForm" % model_class.__name__
    meta = type("Meta", (), {"include": include, "exclude": exclude, "model": model_class})
    kwargs['Meta'] = meta
    return type(name, (ActionModelForm, ), kwargs)