import django_filters
from django import forms
from django.conf import settings
from django.forms import BoundField
from django.urls import reverse

from utilities.forms import widgets
from utilities.utils import get_viewname

__all__ = (
    'DynamicModelChoiceField',
    'DynamicModelMultipleChoiceField',
)


class DynamicModelChoiceMixin:
    """
    Override `get_bound_field()` to avoid pre-populating field choices with a SQL query. The field will be
    rendered only with choices set via bound data. Choices are populated on-demand via the APISelect widget.

    Attributes:
        query_params: A dictionary of additional key/value pairs to attach to the API request
        initial_params: A dictionary of child field references to use for selecting a parent field's initial value
        null_option: The string used to represent a null selection (if any)
        disabled_indicator: The name of the field which, if populated, will disable selection of the
            choice (optional)
        fetch_trigger: The event type which will cause the select element to
            fetch data from the API. Must be 'load', 'open', or 'collapse'. (optional)
    """
    filter = django_filters.ModelChoiceFilter
    widget = widgets.APISelect

    def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None,
                 fetch_trigger=None, empty_label=None, *args, **kwargs):
        self.query_params = query_params or {}
        self.initial_params = initial_params or {}
        self.null_option = null_option
        self.disabled_indicator = disabled_indicator
        self.fetch_trigger = fetch_trigger

        # to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference
        # by widget_attrs()
        self.to_field_name = kwargs.get('to_field_name')
        self.empty_option = empty_label or ""

        super().__init__(*args, **kwargs)

    def widget_attrs(self, widget):
        attrs = {
            'data-empty-option': self.empty_option
        }

        # Set value-field attribute if the field specifies to_field_name
        if self.to_field_name:
            attrs['value-field'] = self.to_field_name

        # Set the string used to represent a null option
        if self.null_option is not None:
            attrs['data-null-option'] = self.null_option

        # Set the disabled indicator, if any
        if self.disabled_indicator is not None:
            attrs['disabled-indicator'] = self.disabled_indicator

        # Set the fetch trigger, if any.
        if self.fetch_trigger is not None:
            attrs['data-fetch-trigger'] = self.fetch_trigger

        # Attach any static query parameters
        if (len(self.query_params) > 0):
            widget.add_query_params(self.query_params)

        return attrs

    def get_bound_field(self, form, field_name):
        bound_field = BoundField(form, self, field_name)

        # Set initial value based on prescribed child fields (if not already set)
        if not self.initial and self.initial_params:
            filter_kwargs = {}
            for kwarg, child_field in self.initial_params.items():
                value = form.initial.get(child_field.lstrip('$'))
                if value:
                    filter_kwargs[kwarg] = value
            if filter_kwargs:
                self.initial = self.queryset.filter(**filter_kwargs).first()

        # Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options
        # will be populated on-demand via the APISelect widget.
        data = bound_field.value()
        if data:
            field_name = getattr(self, 'to_field_name') or 'pk'
            filter = self.filter(field_name=field_name)
            try:
                self.queryset = filter.filter(self.queryset, data)
            except (TypeError, ValueError):
                # Catch any error caused by invalid initial data passed from the user
                self.queryset = self.queryset.none()
        else:
            self.queryset = self.queryset.none()

        # Set the data URL on the APISelect widget (if not already set)
        widget = bound_field.field.widget
        if not widget.attrs.get('data-url'):
            viewname = get_viewname(self.queryset.model, action='list', rest_api=True)
            widget.attrs['data-url'] = reverse(viewname)

        return bound_field


class DynamicModelChoiceField(DynamicModelChoiceMixin, forms.ModelChoiceField):
    """
    Dynamic selection field for a single object, backed by NetBox's REST API.
    """
    def clean(self, value):
        """
        When null option is enabled and "None" is sent as part of a form to be submitted, it is sent as the
        string 'null'.  This will check for that condition and gracefully handle the conversion to a NoneType.
        """
        if self.null_option is not None and value == settings.FILTERS_NULL_CHOICE_VALUE:
            return None
        return super().clean(value)


class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultipleChoiceField):
    """
    A multiple-choice version of `DynamicModelChoiceField`.
    """
    filter = django_filters.ModelMultipleChoiceFilter
    widget = widgets.APISelectMultiple

    def clean(self, value):
        """
        When null option is enabled and "None" is sent as part of a form to be submitted, it is sent as the
        string 'null'.  This will check for that condition and gracefully handle the conversion to a NoneType.
        """
        if self.null_option is not None and settings.FILTERS_NULL_CHOICE_VALUE in value:
            value = [v for v in value if v != settings.FILTERS_NULL_CHOICE_VALUE]
            return [None, *value]
        return super().clean(value)
