import json
import logging
import uuid
from typing import Any

from django.core.exceptions import BadRequest
from django.db.models import (
    Avg,
    Case,
    Exists,
    FloatField,
    IntegerField,
    Max,
    OuterRef,
    Prefetch,
    Q,
    QuerySet,
    Subquery,
    Value,
    When,
)
from django.db.models.functions import Coalesce
from django.http import HttpRequest
from django.utils.http import urlsafe_base64_decode
from django.utils.translation import gettext_lazy as _
from django_filters import (
    BaseInFilter,
    BooleanFilter,
    CharFilter,
    DateTimeFilter,
    FilterSet,
    NumberFilter,
    OrderingFilter,
    UUIDFilter,
)
from graphene import Context
from rest_framework.request import Request

from core.elasticsearch import process_query
from core.models import Address, Brand, Category, Feedback, Order, Product, Stock, Wishlist

logger = logging.getLogger("django")


class CaseInsensitiveListFilter(BaseInFilter, CharFilter):  # type: ignore [misc]
    def filter(self, qs: QuerySet[Any], value: Any) -> QuerySet[Any]:
        if not value:
            return qs

        if isinstance(value, str):
            values = [v.strip() for v in value.split(",") if v.strip()]
        else:
            values = [v for v in value if v]

        if not values:
            return qs

        q = Q()
        for v in values:
            q |= Q(**{f"{self.field_name}__iexact": v})
        else:
            qs = qs.filter(q).distinct()

        return qs


# noinspection PyUnusedLocal
class ProductFilter(FilterSet):  # type: ignore [misc]
    search = CharFilter(field_name="name", method="search_products", label=_("Search"))
    uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID"))
    name = CharFilter(lookup_expr="icontains", label=_("Name"))
    categories = CaseInsensitiveListFilter(field_name="category__name", label=_("Categories"))
    category_uuid = CharFilter(method="filter_category", label="Category (UUID)")
    categories_slugs = CaseInsensitiveListFilter(field_name="category__slug", label=_("Categories Slugs"))
    tags = CaseInsensitiveListFilter(field_name="tags__tag_name", label=_("Tags"))
    min_price = NumberFilter(field_name="stocks__price", lookup_expr="gte", label=_("Min Price"))
    max_price = NumberFilter(field_name="stocks__price", lookup_expr="lte", label=_("Max Price"))
    is_active = BooleanFilter(field_name="is_active", label=_("Is Active"))
    brand = CharFilter(field_name="brand__name", lookup_expr="iexact", label=_("Brand"))
    attributes = CharFilter(method="filter_attributes", label=_("Attributes"))
    quantity = NumberFilter(field_name="stocks__quantity", lookup_expr="gt", label=_("Quantity"))
    slug = CharFilter(field_name="slug", lookup_expr="exact", label=_("Slug"))
    is_digital = BooleanFilter(field_name="is_digital", label=_("Is Digital"))
    include_subcategories = BooleanFilter(method="filter_include_flag", label=_("Include sub-categories"))
    include_personal_ordered = BooleanFilter(
        method="filter_include_personal_ordered",
        label=_("Include personal ordered"),
    )
    sku = CharFilter(field_name="sku", lookup_expr="iexact", label=_("SKU"))

    order_by = OrderingFilter(
        fields=(
            ("uuid", "uuid"),
            ("rating", "rating"),
            ("name", "name"),
            ("slug", "slug"),
            ("created", "created"),
            ("modified", "modified"),
            ("price_order", "price"),
            ("sku", "sku"),
            ("?", "random"),
        ),
        initial="uuid",
    )

    class Meta:
        model = Product
        fields = [
            "uuid",
            "name",
            "categories",
            "category_uuid",
            "attributes",
            "created",
            "modified",
            "is_digital",
            "is_active",
            "tags",
            "slug",
            "sku",
            "min_price",
            "max_price",
            "brand",
            "quantity",
            "order_by",
        ]

    # noinspection PyTypeHints
    def __init__(
        self,
        data: dict[Any, Any] | None = None,
        queryset: QuerySet[Product] | None = None,
        *,
        request: HttpRequest | Request | Context = None,
        prefix: str | None = None,
    ) -> None:
        super().__init__(data=data, queryset=queryset, request=request, prefix=prefix)
        ordering_param = self.data.get("order_by", "")
        if ordering_param:
            order_fields = [field.strip("-") for field in ordering_param.split(",")]
            if "rating" in order_fields:
                feedback_qs = (
                    Feedback.objects.filter(order_product__product_id=OuterRef("pk"))
                    .values("order_product__product_id")
                    .annotate(avg_rating=Avg("rating"))
                    .values("avg_rating")
                )
                self.queryset: QuerySet[Product] = self.queryset.annotate(
                    rating=Coalesce(
                        Subquery(feedback_qs, output_field=FloatField()),
                        Value(0, output_field=FloatField()),
                    )
                )
            if "price" in order_fields:
                self.queryset = self.queryset.annotate(
                    price_order=Coalesce(
                        Max("stocks__price"),
                        Value(0.0),
                        output_field=FloatField(),
                    )
                )

    def search_products(self, queryset: QuerySet[Product], name: str, value: str) -> QuerySet[Product]:
        if not value:
            return queryset

        uuids = [product.get("uuid") for product in process_query(query=value, indexes=("products",))["products"]]  # type: ignore

        return queryset.filter(uuid__in=uuids)

    def filter_include_flag(self, queryset: QuerySet[Product], name: str, value: str) -> QuerySet[Product]:
        if not self.data.get("category_uuid"):
            raise BadRequest(_("there must be a category_uuid to use include_subcategories flag"))
        return queryset

    def filter_include_personal_ordered(self, queryset: QuerySet[Product], name: str, value: str) -> QuerySet[Product]:
        if self.data.get("include_personal_ordered", False):
            queryset = queryset.filter(stocks__isnull=False, stocks__quantity__gt=0, stocks__price__gt=0)
        return queryset

    def filter_attributes(self, queryset: QuerySet[Product], name: str, value: str) -> QuerySet[Product]:
        if not value:
            return queryset

        if str(value).startswith("b64-"):
            value = urlsafe_base64_decode(value[4:]).decode()

        user = getattr(self.request, "user", None)
        can_view_inactive_attrvals = user and user.has_perm("view_attributevalue")

        pairs = [pair.strip() for pair in value.split(";") if "=" in pair]

        q_list = []
        for pair in pairs:
            attr_name, filter_part = pair.split("=", 1)
            attr_name = attr_name.strip()
            filter_part = filter_part.strip()

            if "-" in filter_part:
                method, raw_value = filter_part.split("-", 1)
            else:
                method = "iexact"
                raw_value = filter_part

            method = method.lower().strip()
            raw_value = self._infer_type(raw_value)

            base_filter = Q(**{"attributes__attribute__name__iexact": attr_name})
            if not can_view_inactive_attrvals:
                base_filter &= Q(**{"attributes__is_active": True})

            allowed_methods = {
                "iexact",
                "exact",
                "icontains",
                "contains",
                "isnull",
                "startswith",
                "istartswith",
                "endswith",
                "iendswith",
                "regex",
                "iregex",
                "lt",
                "lte",
                "gt",
                "gte",
                "in",
            }
            if method in allowed_methods:
                field_lookup = f"attributes__value__{method}"
            else:
                field_lookup = "attributes__value__icontains"

            base_filter &= Q(**{field_lookup: raw_value})
            q_list.append(base_filter)

        for q_obj in q_list:
            queryset = queryset.filter(q_obj)

        return queryset

    def filter_category(self, queryset: QuerySet[Product], name: str, value: str) -> QuerySet[Product]:
        if not value:
            return queryset

        include = self.data.get("include_subcategories")
        include_children = str(include).lower() in ("1", "true", "t", "yes", "y")

        try:
            root = Category.objects.get(uuid=value)
        except Category.DoesNotExist:
            return queryset.none()

        if include_children:
            descendants = root.get_descendants(include_self=True)
            return queryset.filter(category__in=descendants)
        else:
            return queryset.filter(category__uuid=value)

    @staticmethod
    def _infer_type(value: str) -> Any:
        try:
            parsed_value = json.loads(value)
            if isinstance(parsed_value, list | dict):
                return parsed_value
        except (json.JSONDecodeError, TypeError):
            pass

        if value.lower() in ["true", "false"]:
            return value.lower() == "true"

        try:
            return int(value)
        except ValueError:
            pass

        try:
            return float(value)
        except ValueError:
            pass

        return value

    @property
    def qs(self) -> QuerySet[Product]:
        qs = super().qs

        ordering_param = self.data.get("order_by", "")

        stock_with_price = Stock.objects.filter(product_id=OuterRef("pk"), price__gt=0)
        stock_with_qty = Stock.objects.filter(product_id=OuterRef("pk"), quantity__gt=0)

        qs = qs.annotate(
            has_price=Exists(stock_with_price),
            has_stock=Exists(stock_with_qty),
        ).annotate(
            personal_order_tail=Case(
                When(Q(has_price=False) | Q(has_stock=False), then=Value(1)),
                default=Value(0),
                output_field=IntegerField(),
            )
        )

        requested = [part.strip() for part in ordering_param.split(",") if part.strip()] if ordering_param else []
        mapped_requested: list[str] = []

        for part in requested:
            desc = part.startswith("-")
            key = part.lstrip("-")

            if key == "price":
                key = "price_order"

            if key == "random":
                mapped_requested.append("?")
                continue

            if key in {"personal_orders_only", "personal_order_only", "personal_order_tail"}:
                continue

            mapped_requested.append(f"-{key}" if desc else key)

        if "?" in mapped_requested:
            final_ordering = ["personal_order_tail", "?"]
        else:
            final_ordering = ["personal_order_tail"] + mapped_requested

        if final_ordering:
            qs = qs.order_by(*final_ordering)

        return qs.distinct()


# noinspection PyUnusedLocal
class OrderFilter(FilterSet):  # type: ignore [misc]
    search = CharFilter(
        method="filter_search",
        label=_("Search (ID, product name or part number)"),
    )

    min_buy_time = DateTimeFilter(field_name="buy_time", lookup_expr="gte", label=_("Bought after (inclusive)"))
    max_buy_time = DateTimeFilter(field_name="buy_time", lookup_expr="lte", label=_("Bought before (inclusive)"))

    uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
    user_email = CharFilter(field_name="user__email", lookup_expr="iexact", label=_("User email"))
    user = UUIDFilter(field_name="user__uuid", lookup_expr="exact", label=_("User UUID"))
    status = CharFilter(field_name="status", lookup_expr="icontains", label=_("Status"))
    human_readable_id = CharFilter(
        field_name="human_readable_id",
        lookup_expr="exact",
        label=_("Human Readable ID"),
    )

    order_by = OrderingFilter(
        fields=(
            ("uuid", "uuid"),
            ("human_readable_id", "human_readable_id"),
            ("user_email", "user_email"),
            ("user", "user"),
            ("status", "status"),
            ("created", "created"),
            ("modified", "modified"),
            ("buy_time", "buy_time"),
            ("?", "random"),
        )
    )

    class Meta:
        model = Order
        fields = [
            "uuid",
            "human_readable_id",
            "user_email",
            "user",
            "status",
            "order_by",
            "search",
            "min_buy_time",
            "max_buy_time",
        ]

    def filter_search(self, queryset: QuerySet[Order], name: str, value: str):
        return queryset.filter(
            Q(human_readable_id__icontains=value)
            | Q(order_products__product__name__icontains=value)
            | Q(order_products__product__partnumber__icontains=value)
        ).distinct()


class WishlistFilter(FilterSet):  # type: ignore [misc]
    uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
    user_email = CharFilter(field_name="user__email", lookup_expr="iexact", label=_("User email"))
    user = UUIDFilter(field_name="user__uuid", lookup_expr="exact", label=_("User UUID"))

    order_by = OrderingFilter(
        fields=(
            ("uuid", "uuid"),
            ("created", "created"),
            ("modified", "modified"),
            ("?", "random"),
        )
    )

    class Meta:
        model = Wishlist
        fields = ["uuid", "user_email", "user", "order_by"]


# noinspection PyUnusedLocal
class CategoryFilter(FilterSet):  # type: ignore [misc]
    search = CharFilter(field_name="name", method="search_categories", label=_("Search"))
    uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
    name = CharFilter(lookup_expr="icontains", label=_("Name"))
    parent_uuid = CharFilter(method="filter_parent_uuid", label=_("Parent"))
    slug = CharFilter(field_name="slug", lookup_expr="exact", label=_("Slug"))
    whole = BooleanFilter(
        field_name="whole",
        label=_("Whole category(has at least 1 product or not)"),
        method="filter_whole_categories",
    )
    tags = CaseInsensitiveListFilter(field_name="tags__tag_name", label=_("Tags"))
    level = NumberFilter(field_name="level", lookup_expr="exact", label=_("Level"))

    order_by = CharFilter(method="filter_order_by")

    class Meta:
        model = Category
        fields = [
            "uuid",
            "name",
            "parent_uuid",
            "slug",
            "tags",
            "level",
            "order_by",
            "whole",
        ]

    def search_categories(self, queryset: QuerySet[Category], name: str, value: str) -> QuerySet[Category]:
        if not value:
            return queryset

        uuids = [category.get("uuid") for category in process_query(query=value, indexes=("categories",))["categories"]]  # type: ignore

        return queryset.filter(uuid__in=uuids)

    def filter_order_by(self, queryset: QuerySet[Category], name: str, value: str) -> QuerySet[Category]:
        if not value:
            return queryset

        desc = value.startswith("-")
        key = value.lstrip("-")

        mapping = {
            "priority": "priority",
            "uuid": "uuid",
            "name": "name",
            "?": "random",
        }
        field = mapping.get(key)
        if field is None:
            return queryset

        if field == "?":
            order_expression = "?"
        else:
            order_expression = f"-{field}" if desc else field

        qs = queryset.order_by(order_expression).prefetch_related(None)

        def create_ordered_tree_prefetch(max_depth=10) -> Prefetch | None:
            if field == "?":

                def build_random_prefetch(depth):
                    if depth <= 0:
                        return None

                    children_qs = Category.objects.all().order_by("?")
                    nested_prefetch = build_random_prefetch(depth - 1)

                    if nested_prefetch:
                        children_qs = children_qs.prefetch_related(nested_prefetch)

                    return Prefetch("children", queryset=children_qs)

                return build_random_prefetch(max_depth)
            else:

                def build_ordered_prefetch(depth):
                    if depth <= 0:
                        return None

                    children_qs = Category.objects.all().order_by(order_expression, "tree_id", "lft")
                    nested_prefetch = build_ordered_prefetch(depth - 1)

                    if nested_prefetch:
                        children_qs = children_qs.prefetch_related(nested_prefetch)

                    return Prefetch("children", queryset=children_qs)

                return build_ordered_prefetch(max_depth)

        tree_prefetch = create_ordered_tree_prefetch()
        if tree_prefetch:
            qs = qs.prefetch_related(tree_prefetch)

        return qs

    def filter_whole_categories(self, queryset: QuerySet[Category], name: str, value: str) -> QuerySet[Category]:
        has_own_products = Exists(Product.objects.filter(category=OuterRef("pk")))
        has_desc_products = Exists(
            Product.objects.filter(
                is_active=True,
                category__tree_id=OuterRef("tree_id"),
                category__lft__gt=OuterRef("lft"),
                category__rght__lt=OuterRef("rght"),
            )
        )
        annotated = queryset.annotate(has_products=has_own_products | has_desc_products)
        if value:
            return annotated.filter(has_products=True).distinct()
        return annotated.filter(has_products=False).distinct()

    def filter_parent_uuid(self, queryset: QuerySet[Category], name: str, value: str):
        if value in ("", "null", "None"):
            return queryset.filter(parent=None)

        try:
            uuid_val = uuid.UUID(value)
        except (ValueError, TypeError):
            return queryset

        return queryset.filter(parent__uuid=uuid_val)


# noinspection PyUnusedLocal
class BrandFilter(FilterSet):  # type: ignore [misc]
    search = CharFilter(field_name="name", method="search_brands", label=_("Search"))
    uuid = UUIDFilter(field_name="uuid", lookup_expr="exact")
    name = CharFilter(lookup_expr="icontains", label=_("Name"))
    slug = CharFilter(field_name="slug", lookup_expr="exact", label=_("Slug"))
    categories = CaseInsensitiveListFilter(field_name="categories__uuid", lookup_expr="exact", label=_("Categories"))

    order_by = OrderingFilter(
        fields=(
            ("priority", "priority"),
            ("uuid", "uuid"),
            ("slug", "slug"),
            ("name", "name"),
            ("?", "random"),
        )
    )

    class Meta:
        model = Brand
        fields = ["uuid", "name", "slug", "priority"]

    def search_brands(self, queryset: QuerySet[Brand], name: str, value: str) -> QuerySet[Brand]:
        if not value:
            return queryset

        uuids = [brand.get("uuid") for brand in process_query(query=value, indexes=("brands",))["brands"]]  # type: ignore

        return queryset.filter(uuid__in=uuids)


class FeedbackFilter(FilterSet):  # type: ignore [misc]
    uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID"))
    product_uuid = UUIDFilter(
        field_name="order_product__product__uuid",
        lookup_expr="exact",
        label=_("Product UUID"),
    )
    user_uuid = UUIDFilter(
        field_name="order_product__order__user__uuid",
        lookup_expr="exact",
        label=_("User UUID"),
    )

    order_by = OrderingFilter(
        fields=(
            ("uuid", "uuid"),
            ("product", "product"),
            ("rating", "rating"),
            ("created", "created"),
            ("modified", "modified"),
            ("?", "random"),
        )
    )

    class Meta:
        model = Feedback
        fields = ["uuid", "product_uuid", "user_uuid", "order_by"]


class AddressFilter(FilterSet):  # type: ignore [misc]
    uuid = UUIDFilter(field_name="uuid", lookup_expr="exact", label=_("UUID"))
    user_uuid = UUIDFilter(field_name="user__uuid", lookup_expr="exact", label=_("User UUID"))
    user_email = CharFilter(field_name="user__email", lookup_expr="iexact", label=_("User email"))

    order_by = OrderingFilter(
        fields=(
            ("uuid", "uuid"),
            ("user__email", "user_email"),
            ("user__uuid", "user_uuid"),
            ("created", "created"),
            ("modified", "modified"),
            ("?", "random"),
        )
    )

    class Meta:
        model = Address
        fields = ["uuid", "user_uuid", "user_email", "order_by"]
