from django import forms
from django_filters import (
    FilterSet,
    CharFilter,
    DateFilter,
    NumberFilter,
    BooleanFilter,
    ModelChoiceFilter,
    TimeFilter,
    ModelMultipleChoiceFilter,
)
from django.db import models

from powercrud.conf import get_powercrud_setting
from powercrud.logging import get_logger
from .config_mixin import resolve_config

log = get_logger(__name__)


class AllValuesModelMultipleChoiceFilter(ModelMultipleChoiceFilter):
    """Custom filter that requires ALL selected values to match (AND logic)"""

    def filter(self, qs, value):
        if not value:
            return qs

        # For each value, filter for items that have that value in the M2M field
        for val in value:
            qs = qs.filter(**{f"{self.field_name}": val})
        return qs


class HTMXFilterSetMixin:
    """
    Mixin that adds HTMX attributes to filter forms for dynamic updates.

    Attributes:
        HTMX_ATTRS (dict): Base HTMX attributes for form fields
        FIELD_TRIGGERS (dict): Mapping of form field types to HTMX trigger events
    """

    HTMX_ATTRS: dict[str, str] = {
        "hx-get": "",
        "hx-include": "[name]",  # Include all named form fields
    }

    FIELD_TRIGGERS: dict[type[forms.Widget] | str, str] = {
        forms.DateInput: "change",
        forms.TextInput: "keyup changed delay:300ms",
        forms.NumberInput: "keyup changed delay:300ms",
        "default": "change",
    }

    def setup_htmx_attrs(self) -> None:
        """Configure HTMX attributes for form fields and setup crispy form helper."""
        for field in self.form.fields.values():
            widget_class: type[forms.Widget] = type(field.widget)

            trigger: str = self.FIELD_TRIGGERS.get(
                widget_class, self.FIELD_TRIGGERS["default"]
            )

            attrs: dict[str, str] = {**self.HTMX_ATTRS, "hx-trigger": trigger}

            field.widget.attrs.update(attrs)


class FilteringMixin:
    """
    Provides dynamic FilterSet generation for powercrud views.
    """

    def get_filter_queryset_for_field(self, field_name, model_field):
        """Get an efficiently filtered and sorted queryset for filter options."""

        # Start with an empty queryset
        queryset = model_field.related_model.objects

        # Define model_fields early to ensure it exists in all code paths
        model_fields = [f.name for f in model_field.related_model._meta.fields]

        # Apply custom filters if defined
        filter_options = getattr(self, "filter_queryset_options", {})
        if field_name in filter_options:
            filters = filter_options[field_name]
            if callable(filters):
                try:
                    # Add error handling for the callable
                    result = filters(self.request, field_name, model_field)
                    if isinstance(result, models.QuerySet):
                        queryset = result
                    else:
                        queryset = queryset.filter(**result)
                except Exception as e:
                    import logging

                    logging.error(
                        f"Error in filter callable for {field_name}: {str(e)}"
                    )
            elif isinstance(filters, dict):
                # Apply filter dict directly
                queryset = queryset.filter(**filters)
            elif isinstance(filters, (int, str)):
                # Handle simple ID/PK filtering
                queryset = queryset.filter(pk=filters)
        else:
            # No filters specified, get all records
            queryset = queryset.all()

        # Check if we should sort by a specific field
        sort_options = resolve_config(self).dropdown_sort_options
        if field_name in sort_options:
            sort_field = sort_options[field_name]  # Can be "name" or "-name"
            return queryset.order_by(sort_field)

        # If no specified sort field but model has common name fields, use that
        for field in ["name", "title", "label", "display_name"]:
            if field in model_fields:
                return queryset.order_by(field)

        # Only if really necessary, fall back to string representation sorting
        sorted_objects = sorted(list(queryset), key=lambda x: str(x).lower())
        pk_list = [obj.pk for obj in sorted_objects]

        if not pk_list:  # Empty list case
            return queryset.none()

        # Return ordered queryset
        from django.db.models import Case, When, Value, IntegerField

        preserved_order = Case(
            *[When(pk=pk, then=Value(i)) for i, pk in enumerate(pk_list)],
            output_field=IntegerField(),
        )

        return queryset.filter(pk__in=pk_list).order_by(preserved_order)

    def get_filterset(self, queryset=None):  # pragma: no cover
        """
        Create a dynamic FilterSet class based on provided parameters:
            - filterset_class (in which case the provided class is used); or
            - filterset_fields (in which case a dynamic class is created)

        Args:
            queryset: Optional queryset to filter

        Returns:
            FilterSet: Configured filter set instance or None
        """
        filterset_class = getattr(self, "filterset_class", None)
        filterset_fields = getattr(self, "filterset_fields", None)

        if filterset_class is not None or filterset_fields is not None:
            # Check if any filter params (besides page/sort) are present
            filter_keys = [
                k
                for k in self.request.GET.keys()
                if k not in ("page", "sort", "page_size")
            ]

            # Only reset pagination for actual filter form submissions
            is_filter_form_submission = (
                self.request.headers.get("X-Filter-Setting-Request") == "true"
            )

            if filter_keys and "page" in self.request.GET and is_filter_form_submission:
                setattr(self, "_reset_pagination", True)

        if filterset_class is None and filterset_fields is not None:
            use_htmx = self.get_use_htmx()

            class DynamicFilterSet(HTMXFilterSetMixin, FilterSet):
                """
                Dynamically create a FilterSet class based on the model fields.
                This class inherits from HTMXFilterSetMixin to add HTMX functionality
                and FilterSet for Django filtering capabilities.
                """

                framework = get_powercrud_setting("POWERCRUD_CSS_FRAMEWORK")
                BASE_ATTRS = self.get_framework_styles()[framework]["filter_attrs"]

                # Dynamically create filter fields based on the model's fields
                for field_name in filterset_fields:
                    model_field = self.model._meta.get_field(field_name)

                    # Handle GeneratedField special case
                    if hasattr(models, "GeneratedField") and isinstance(
                        model_field, models.GeneratedField
                    ):
                        field_to_check = model_field.output_field
                    else:
                        field_to_check = model_field
                    # Check if BASE_ATTRS is structured by field type
                    if isinstance(BASE_ATTRS, dict) and (
                        "text" in BASE_ATTRS or "select" in BASE_ATTRS
                    ):
                        # Get appropriate attributes based on field type
                        if isinstance(field_to_check, models.ManyToManyField):
                            field_attrs = BASE_ATTRS.get(
                                "multiselect",
                                BASE_ATTRS.get("select", BASE_ATTRS.get("default", {})),
                            ).copy()
                        elif isinstance(field_to_check, models.ForeignKey):
                            field_attrs = BASE_ATTRS.get(
                                "select", BASE_ATTRS.get("default", {})
                            ).copy()
                        elif isinstance(
                            field_to_check, (models.CharField, models.TextField)
                        ):
                            field_attrs = BASE_ATTRS.get(
                                "text", BASE_ATTRS.get("default", {})
                            ).copy()
                        elif isinstance(field_to_check, models.DateField):
                            field_attrs = BASE_ATTRS.get(
                                "date", BASE_ATTRS.get("default", {})
                            ).copy()
                        elif isinstance(
                            field_to_check,
                            (
                                models.IntegerField,
                                models.DecimalField,
                                models.FloatField,
                            ),
                        ):
                            field_attrs = BASE_ATTRS.get(
                                "number", BASE_ATTRS.get("default", {})
                            ).copy()
                        elif isinstance(field_to_check, models.TimeField):
                            field_attrs = BASE_ATTRS.get(
                                "time", BASE_ATTRS.get("default", {})
                            ).copy()
                        elif isinstance(field_to_check, models.BooleanField):
                            field_attrs = BASE_ATTRS.get(
                                "select", BASE_ATTRS.get("default", {})
                            ).copy()
                        else:
                            field_attrs = BASE_ATTRS.get("default", {}).copy()
                    else:
                        # Legacy behavior - use the same attributes for all fields
                        field_attrs = BASE_ATTRS.copy()

                    # Create appropriate filter based on field type
                    if isinstance(field_to_check, models.ManyToManyField):
                        # Add max-height and other useful styles to the select widget
                        field_attrs.update(
                            {
                                "style": "max-height: 200px; overflow-y: auto;",
                                "class": field_attrs.get("class", "")
                                + " select2",  # Add select2 class if you want to use Select2
                            }
                        )

                        # Choose between OR logic (ModelMultipleChoiceFilter) or AND logic (AllValuesModelMultipleChoiceFilter)
                        filter_class = (
                            AllValuesModelMultipleChoiceFilter
                            if resolve_config(self).m2m_filter_and_logic
                            else ModelMultipleChoiceFilter
                        )

                        locals()[field_name] = filter_class(
                            queryset=self.get_filter_queryset_for_field(
                                field_name, model_field
                            ),
                            widget=forms.SelectMultiple(attrs=field_attrs),
                        )
                    elif isinstance(
                        field_to_check, (models.CharField, models.TextField)
                    ):
                        locals()[field_name] = CharFilter(
                            lookup_expr="icontains",
                            widget=forms.TextInput(attrs=field_attrs),
                        )
                    elif isinstance(field_to_check, models.DateField):
                        if "type" not in field_attrs:
                            field_attrs["type"] = "date"
                        locals()[field_name] = DateFilter(
                            widget=forms.DateInput(attrs=field_attrs)
                        )
                    elif isinstance(
                        field_to_check,
                        (models.IntegerField, models.DecimalField, models.FloatField),
                    ):
                        if "step" not in field_attrs:
                            field_attrs["step"] = "any"
                        locals()[field_name] = NumberFilter(
                            widget=forms.NumberInput(attrs=field_attrs)
                        )
                    elif isinstance(field_to_check, models.BooleanField):
                        locals()[field_name] = BooleanFilter(
                            widget=forms.Select(
                                attrs=field_attrs,
                                choices=(
                                    (None, "---------"),
                                    (True, True),
                                    (False, False),
                                ),
                            )
                        )
                    elif isinstance(field_to_check, models.ForeignKey):
                        locals()[field_name] = ModelChoiceFilter(
                            queryset=self.get_filter_queryset_for_field(
                                field_name, model_field
                            ),
                            widget=forms.Select(attrs=field_attrs),
                        )
                    elif isinstance(field_to_check, models.TimeField):
                        if "type" not in field_attrs:
                            field_attrs["type"] = "time"
                        locals()[field_name] = TimeFilter(
                            widget=forms.TimeInput(attrs=field_attrs)
                        )
                    else:
                        locals()[field_name] = CharFilter(
                            lookup_expr="icontains",
                            widget=forms.TextInput(attrs=field_attrs),
                        )

                class Meta:
                    model = self.model
                    fields = filterset_fields

                def __init__(self, *args, **kwargs):
                    """Initialize the FilterSet and set up HTMX attributes if needed."""
                    super().__init__(*args, **kwargs)
                    if use_htmx:
                        self.setup_htmx_attrs()

            filterset_class = DynamicFilterSet

        if filterset_class is None:
            return None

        return filterset_class(
            self.request.GET,
            queryset=queryset,
            request=self.request,
        )
