import requests
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import Distance
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_filters import (
    BooleanFilter,
    CharFilter,
    ChoiceFilter,
    DateFromToRangeFilter,
    FilterSet,
    ModelChoiceFilter,
    NumberFilter,
    OrderingFilter,
)

from apps.core.choices.registry import get_choices

from .models import Company, Park
from .querysets import get_base_park_queryset


def validate_positive_integer(value):
    """Validate that a value is a positive integer"""
    try:
        value = float(value)
        if not value.is_integer() or value < 0:
            raise ValidationError(_("Value must be a positive integer"))
        return int(value)
    except (TypeError, ValueError):
        raise ValidationError(_("Invalid number format")) from None


class ParkFilter(FilterSet):
    """Filter set for parks with search and validation capabilities"""

    class Meta:
        model = Park
        fields = []

    # Search field with better description
    search = CharFilter(
        method="filter_search",
        label=_("Search Parks"),
        help_text=_("Search by park name, description, or location"),
    )

    # Status filter with clearer label
    status = ChoiceFilter(
        field_name="status",
        choices=lambda: [(choice.value, choice.label) for choice in get_choices("park_statuses", "parks")],
        empty_label=_("Any status"),
        label=_("Operating Status"),
        help_text=_("Filter parks by their current operating status"),
    )

    # Operator filters with helpful descriptions
    operator = ModelChoiceFilter(
        field_name="operator",
        queryset=Company.objects.filter(roles__contains=["OPERATOR"]),
        empty_label=_("Any operator"),
        label=_("Operating Company"),
        help_text=_("Filter parks by their operating company"),
    )
    has_operator = BooleanFilter(
        method="filter_has_operator",
        label=_("Operator Status"),
        help_text=_("Show parks with or without an operating company"),
    )

    # Ride and attraction filters
    min_rides = NumberFilter(
        field_name="ride_count",
        lookup_expr="gte",
        validators=[validate_positive_integer],
        label=_("Minimum Rides"),
        help_text=_("Show parks with at least this many rides"),
    )
    min_coasters = NumberFilter(
        field_name="coaster_count",
        lookup_expr="gte",
        validators=[validate_positive_integer],
        label=_("Minimum Roller Coasters"),
        help_text=_("Show parks with at least this many roller coasters"),
    )

    # Size filter
    min_size = NumberFilter(
        field_name="size_acres",
        lookup_expr="gte",
        validators=[validate_positive_integer],
        label=_("Minimum Size (acres)"),
        help_text=_("Show parks of at least this size in acres"),
    )

    # Opening date filter with better label
    opening_date = DateFromToRangeFilter(
        field_name="opening_date",
        label=_("Opening Date Range"),
        help_text=_("Filter parks by their opening date"),
    )

    # Location-based filters
    location_search = CharFilter(
        method="filter_location_search",
        label=_("Location Search"),
        help_text=_("Search by city, state, country, or address"),
    )

    near_location = CharFilter(
        method="filter_near_location",
        label=_("Near Location"),
        help_text=_("Find parks near a specific location"),
    )

    radius_km = NumberFilter(
        method="filter_radius",
        label=_("Radius (km)"),
        help_text=_("Search radius in kilometers (use with 'Near Location')"),
    )

    country_filter = CharFilter(
        method="filter_country",
        label=_("Country"),
        help_text=_("Filter parks by country"),
    )

    state_filter = CharFilter(
        method="filter_state",
        label=_("State/Region"),
        help_text=_("Filter parks by state or region"),
    )

    # Practical filter fields that people actually use
    park_type = ChoiceFilter(
        method="filter_park_type",
        label=_("Park Type"),
        help_text=_("Filter by popular park categories"),
        choices=[
            ("disney", _("Disney Parks")),
            ("universal", _("Universal Parks")),
            ("six_flags", _("Six Flags")),
            ("cedar_fair", _("Cedar Fair")),
            ("independent", _("Independent Parks")),
        ],
        empty_label=_("All parks"),
    )

    has_coasters = BooleanFilter(
        method="filter_has_coasters",
        label=_("Has Roller Coasters"),
        help_text=_("Show only parks with roller coasters"),
    )

    min_rating = ChoiceFilter(
        method="filter_min_rating",
        label=_("Minimum Rating"),
        help_text=_("Show parks with at least this rating"),
        choices=[
            ("3", _("3+ stars")),
            ("4", _("4+ stars")),
            ("4.5", _("4.5+ stars")),
        ],
        empty_label=_("Any rating"),
    )

    big_parks_only = BooleanFilter(
        method="filter_big_parks",
        label=_("Major Parks Only"),
        help_text=_("Show only large theme parks (10+ rides)"),
    )

    # Simple, useful ordering
    ordering = OrderingFilter(
        fields=(
            ("name", "name"),
            ("average_rating", "rating"),
            ("coaster_count", "coasters"),
            ("ride_count", "rides"),
        ),
        field_labels={
            "name": _("Name (A-Z)"),
            "-name": _("Name (Z-A)"),
            "-average_rating": _("Highest Rated"),
            "-coaster_count": _("Most Coasters"),
            "-ride_count": _("Most Rides"),
        },
        label=_("Sort by"),
    )

    def filter_search(self, queryset, name, value):
        """Custom search implementation"""
        if not value:
            return queryset

        search_fields = [
            "name__icontains",
            "description__icontains",
            "location__city__icontains",
            "location__state__icontains",
            "location__country__icontains",
        ]

        queries = [models.Q(**{field: value}) for field in search_fields]
        query = queries.pop()
        for item in queries:
            query |= item

        return queryset.filter(query).distinct()

    def filter_has_operator(self, queryset, name, value):
        """Filter parks based on whether they have an operator"""
        return queryset.filter(operator__isnull=not value)

    @property
    def qs(self):
        """
        Override qs property to ensure we always use base queryset with annotations
        """
        if not hasattr(self, "_qs"):
            # Start with optimized base queryset
            base_qs = (
                get_base_park_queryset()
                .select_related("operator", "property_owner", "location")
                .prefetch_related("photos", "rides__manufacturer")
            )

            if not self.is_bound:
                self._qs = base_qs
                return self._qs

            if not self.form.is_valid():
                self._qs = base_qs.none()
                return self._qs

            self._qs = base_qs
            for name, value in self.form.cleaned_data.items():
                if value in [None, "", 0] and name not in [
                    "has_operator",
                    "has_coasters",
                    "big_parks_only",
                ]:
                    continue
                self._qs = self.filters[name].filter(self._qs, value)
            self._qs = self._qs.distinct()
        return self._qs

    def filter_location_search(self, queryset, name, value):
        """Filter parks by location fields"""
        if not value:
            return queryset

        location_query = (
            models.Q(location__city__icontains=value)
            | models.Q(location__state__icontains=value)
            | models.Q(location__country__icontains=value)
            | models.Q(location__street_address__icontains=value)
        )

        return queryset.filter(location_query).distinct()

    def filter_near_location(self, queryset, name, value):
        """Filter parks near a specific location using geocoding"""
        if not value:
            return queryset

        # Try to geocode the location
        coordinates = self._geocode_location(value)
        if not coordinates:
            return queryset

        lat, lng = coordinates
        point = Point(lng, lat, srid=4326)

        # Get radius from form data, default to 50km
        radius = self.data.get("radius_km", 50)
        try:
            radius = float(radius)
        except (ValueError, TypeError):
            radius = 50

        # Filter by distance
        distance = Distance(km=radius)
        return (
            queryset.filter(location__point__distance_lte=(point, distance))
            .annotate(distance=models.Value(0, output_field=models.FloatField()))
            .order_by("distance")
            .distinct()
        )

    def filter_radius(self, queryset, name, value):
        """Radius filter - handled by filter_near_location"""
        return queryset

    def filter_country(self, queryset, name, value):
        """Filter parks by country"""
        if not value:
            return queryset
        return queryset.filter(location__country__icontains=value).distinct()

    def filter_state(self, queryset, name, value):
        """Filter parks by state/region"""
        if not value:
            return queryset
        return queryset.filter(location__state__icontains=value).distinct()

    def filter_park_type(self, queryset, name, value):
        """Filter parks by popular company/brand"""
        if not value:
            return queryset

        # Map common park types to operator name patterns
        type_filters = {
            "disney": models.Q(operator__name__icontains="Disney"),
            "universal": models.Q(operator__name__icontains="Universal"),
            "six_flags": models.Q(operator__name__icontains="Six Flags"),
            "cedar_fair": models.Q(operator__name__icontains="Cedar Fair")
            | models.Q(operator__name__icontains="Cedar Point")
            | models.Q(operator__name__icontains="Kings Island")
            | models.Q(operator__name__icontains="Canada's Wonderland"),
            "independent": ~(
                models.Q(operator__name__icontains="Disney")
                | models.Q(operator__name__icontains="Universal")
                | models.Q(operator__name__icontains="Six Flags")
                | models.Q(operator__name__icontains="Cedar Fair")
                | models.Q(operator__name__icontains="Cedar Point")
            ),
        }

        if value in type_filters:
            return queryset.filter(type_filters[value])

        return queryset

    def filter_has_coasters(self, queryset, name, value):
        """Filter parks based on whether they have roller coasters"""
        if value is None:
            return queryset

        if value:
            return queryset.filter(coaster_count__gt=0)
        else:
            return queryset.filter(models.Q(coaster_count__isnull=True) | models.Q(coaster_count=0))

    def filter_min_rating(self, queryset, name, value):
        """Filter parks by minimum rating"""
        if not value:
            return queryset

        try:
            min_rating = float(value)
            return queryset.filter(average_rating__gte=min_rating)
        except (ValueError, TypeError):
            return queryset

    def filter_big_parks(self, queryset, name, value):
        """Filter to show only major parks with many rides"""
        if not value:
            return queryset

        return queryset.filter(ride_count__gte=10)

    def _geocode_location(self, location_string):
        """
        Geocode a location string using OpenStreetMap Nominatim.
        Returns (lat, lng) tuple or None if geocoding fails.
        """
        try:
            response = requests.get(
                "https://nominatim.openstreetmap.org/search",
                params={
                    "q": location_string,
                    "format": "json",
                    "limit": 1,
                    "countrycodes": "us,ca,gb,fr,de,es,it,jp,au",  # Popular countries
                },
                headers={"User-Agent": "ThrillWiki/1.0"},
                timeout=5,
            )

            if response.status_code == 200:
                data = response.json()
                if data:
                    result = data[0]
                    return float(result["lat"]), float(result["lon"])
        except Exception:
            # Silently fail geocoding - just return None
            pass

        return None
