from datetime import timedelta
from typing import Any, TYPE_CHECKING

from django.conf import settings
from django.db.models import Q, QuerySet
from django_filters import rest_framework as filters
from .forms import DateRangeFilterField

if TYPE_CHECKING:
    from console_base.models import BaseUUIDPKModel


# ---------------------------------------------------------------------------
class LCDateFromToRangeFilter(filters.CharFilter):
    field_class = DateRangeFilterField

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self.lookup_expr = 'range'


# ---------------------------------------------------------------------------
class LCDateHashFromToRangeFilter(filters.CharFilter):
    """
    Get range values for compatibility with DateHash partitioned tables.
    """

    field_class = DateRangeFilterField

    def filter(self, qs: QuerySet, value: Any) -> QuerySet:
        if not value:
            return qs

        start, end = value

        # `__range` dates are "less-than", so add a day to include
        # all values less than the day after the `end` date.
        end += timedelta(days=1)

        return qs.filter(
            Q(
                (f'{self.field_name}__range', (start.strftime('%Y%m%d'), end.strftime('%Y%m%d'))),
            )
        )


# ---------------------------------------------------------------------------
class UUIDInFilter(filters.BaseInFilter, filters.UUIDFilter):
    pass


# ---------------------------------------------------------------------------
class StringInFilter(filters.BaseInFilter, filters.CharFilter):
    pass


# ---------------------------------------------------------------------------
class NumberInFilter(filters.BaseInFilter, filters.NumberFilter):
    pass


# ---------------------------------------------------------------------------
class LCFilterSet(filters.FilterSet):
    def __init__(self, *args: Any, **kwargs: Any) -> None:
        """
        Exclude filters must have `<fieldname>_exclude` naming convention so we can
        generate Q objects from the same dictionaries used to pass query params to
        requests library API calls
        """
        super().__init__(*args, **kwargs)

        errors = []
        name = self.__class__.__name__
        for k, v in self.filters.items():
            if not v.exclude:
                continue
            if not k.endswith(settings.DRF_EXCLUDE_SUFFIX):
                errors.append(
                    f'{k} field on {name} FilterSet class does '
                    f'not have "{settings.DRF_EXCLUDE_SUFFIX}" suffix',
                )
        if errors:
            raise ValueError('\n'.join(errors))


# ---------------------------------------------------------------------------
class DateFilter(LCFilterSet):
    """
    Base Filter class. All serializers must include these fields!
    """

    created = filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
    modified = filters.IsoDateTimeFilter(field_name='modified', lookup_expr='gte')
    is_active = filters.BooleanFilter(field_name='is_active')
    cid = UUIDInFilter(field_name='cid', lookup_expr='in')

    class Meta:
        fields: tuple[str, ...] = ('created', 'modified', 'cid')
        if TYPE_CHECKING:
            model: type['BaseUUIDPKModel']


# ---------------------------------------------------------------------------
class NameFilter(DateFilter):
    """
    Base Filter class. All serializers must include these fields!
    """

    # Does the list view displays the pretty name? If so, we'll
    # need to convert spaces to underscores for correct searching
    pretty_name_displayed = False

    name = filters.CharFilter(field_name='name', method='filter_name', label='Name contains')
    name__exact = filters.CharFilter(field_name='name', method='get_name', label='Name Equals')

    class Meta(DateFilter.Meta):
        fields = DateFilter.Meta.fields + ('name',)

    def name_fields(self) -> list[str]:
        name_fields: list[str] = []
        model_fields = self.Meta.model.get_field_names(self.Meta.model)  # type: ignore[arg-type]

        name_fields.extend(model_fields.intersection(settings.TEXT_NAME_FIELDS))

        name_fields.extend([
            f'{field}__name' for field in model_fields.intersection(settings.FK_NAME_FIELDS)
        ])

        return name_fields

    def filter_name(self, queryset: QuerySet, name: str, value: Any) -> QuerySet:  # noqa
        value_underscored = value.replace(' ', '_') if self.pretty_name_displayed else value

        q = Q()
        for field in self.name_fields():
            q |= Q((f'{field}__icontains', value))
            if self.pretty_name_displayed:
                q |= Q((f'{field}__icontains', value_underscored))
        return queryset.filter(q)

    def get_name(self, queryset: QuerySet, name: str, value: Any) -> QuerySet:  # noqa
        q = Q()
        for field in self.name_fields():
            q |= Q((f'{field}', value))
        return queryset.filter(q)


# ---------------------------------------------------------------------------
class CompanyFilter(NameFilter):
    """
    Most console records contain a Company field,
    so this filter should be used on those records
    """

    company = filters.UUIDFilter(field_name='company__id', lookup_expr='exact')
    company__cid = UUIDInFilter(field_name='company__cid', lookup_expr='in')
    company__isnull = filters.BooleanFilter(
        field_name='company',
        lookup_expr='isnull',
        label='Company Is Null',
    )

    class Meta(NameFilter.Meta):
        fields = NameFilter.Meta.fields + (
            'company',
            'company__cid',
            'company__isnull',
        )


__all__ = (
    'CompanyFilter',
    'DateFilter',
    'LCFilterSet',
    'NameFilter',
    'NumberInFilter',
    'StringInFilter',
    'UUIDInFilter',
    'LCDateFromToRangeFilter',
    'LCDateHashFromToRangeFilter',
)
