import logging
from typing import Any

from constance import config
from django.core.cache import cache
from django.db.models import Max, Min, QuerySet
from django.db.models.functions import Length
from django.utils.translation import gettext_lazy as _
from graphene import (
    UUID,
    Boolean,
    Field,
    Float,
    InputObjectType,
    Int,
    List,
    NonNull,
    ObjectType,
    String,
    relay,
)
from graphene.types.generic import GenericScalar
from graphene_django import DjangoObjectType
from graphene_django.filter import DjangoFilterConnectionField
from graphene_django.utils import camelize
from mptt.querysets import TreeQuerySet

from core.models import (
    Address,
    Attribute,
    AttributeGroup,
    AttributeValue,
    Brand,
    Category,
    CategoryTag,
    Feedback,
    Order,
    OrderProduct,
    Product,
    ProductImage,
    ProductTag,
    PromoCode,
    Promotion,
    Stock,
    Vendor,
    Wishlist,
)
from core.utils import graphene_abs, graphene_current_lang
from core.utils.seo_builders import (
    brand_schema,
    breadcrumb_schema,
    category_schema,
    item_list_schema,
    org_schema,
    product_schema,
    website_schema,
)
from payments.graphene.object_types import TransactionType

logger = logging.getLogger("django")


class SEOMetaType(ObjectType):  # type: ignore [misc]
    title = String()
    description = String()
    canonical = String()
    robots = String()
    open_graph = GenericScalar()
    twitter = GenericScalar()
    json_ld = List(GenericScalar)
    hreflang = String()


class AttributeType(DjangoObjectType):  # type: ignore [misc]
    values = List(lambda: AttributeValueType, description=_("attribute values"))

    class Meta:
        model = Attribute
        interfaces = (relay.Node,)
        fields = ("uuid", "value_type", "name")
        filter_fields = ["uuid"]
        description = _("attributes")

    def resolve_values(self, info) -> QuerySet[AttributeValue]:
        base_qs = AttributeValue.objects.filter(attribute=self)
        product_uuid = getattr(info.context, "_product_uuid", None)

        if product_uuid:
            base_qs = base_qs.filter(product__uuid=product_uuid)

        return base_qs


class AttributeGroupType(DjangoObjectType):  # type: ignore [misc]
    attributes = List(lambda: AttributeType, description=_("grouped attributes"))

    class Meta:
        model = AttributeGroup
        interfaces = (relay.Node,)
        fields = ("uuid", "parent", "name", "attributes")
        filter_fields = ["uuid"]
        description = _("groups of attributes")

    def resolve_attributes(self: AttributeGroup, info) -> QuerySet[Attribute]:
        product_uuid = getattr(info.context, "_product_uuid", None)

        qs = self.attributes.all()

        if product_uuid:
            qs = qs.filter(values__product__uuid=product_uuid).distinct()

        return qs


class BrandType(DjangoObjectType):  # type: ignore [misc]
    categories = List(lambda: CategoryType, description=_("categories"))
    seo_meta = Field(SEOMetaType, description=_("SEO Meta snapshot"))

    class Meta:
        model = Brand
        interfaces = (relay.Node,)
        fields = ("uuid", "categories", "name", "description", "big_logo", "small_logo", "slug")
        filter_fields = ["uuid", "name"]
        description = _("brands")

    def resolve_categories(self: Brand, info) -> QuerySet[Category]:
        if info.context.user.has_perm("core.view_category"):
            return self.categories.all()
        return self.categories.filter(is_active=True)

    def resolve_big_logo(self: Brand, info) -> str | None:
        return info.context.build_absolute_uri(self.big_logo.url) if self.big_logo else ""

    def resolve_small_logo(self: Brand, info) -> str | None:
        return info.context.build_absolute_uri(self.small_logo.url) if self.small_logo else ""

    def resolve_seo_meta(self: Brand, info) -> dict[str, str | list[Any] | dict[str, str] | None]:
        lang = graphene_current_lang()
        base = f"https://{config.BASE_DOMAIN}"
        canonical = f"{base}/{lang}/brand/{self.slug}"
        title = f"{self.name} | {config.PROJECT_NAME}"
        description = (self.description or "")[:180]

        logo_url = None
        if getattr(self, "big_logo", None):
            logo_url = graphene_abs(info.context, self.big_logo.url)
        elif getattr(self, "small_logo", None):
            logo_url = graphene_abs(info.context, self.small_logo.url)

        og = {
            "title": title,
            "description": description,
            "type": "website",
            "url": canonical,
            "image": logo_url or "",
        }
        tw = {"card": "summary_large_image", "title": title, "description": description, "image": logo_url or ""}

        crumbs = [("Home", f"{base}/"), (self.name, canonical)]

        json_ld = [
            org_schema(),
            website_schema(),
            breadcrumb_schema(crumbs),
            brand_schema(self, canonical, logo_url=logo_url),
        ]

        return {
            "title": title,
            "description": description,
            "canonical": canonical,
            "robots": "index,follow",
            "open_graph": og,
            "twitter": tw,
            "json_ld": json_ld,
            "hreflang": info.context.LANGUAGE_CODE,
        }


class FilterableAttributeType(ObjectType):  # type: ignore [misc]
    attribute_name = String(required=True)
    possible_values = List(String, required=True)


class MinMaxPriceType(ObjectType):  # type: ignore [misc]
    min_price = Float()
    max_price = Float()


class CategoryType(DjangoObjectType):  # type: ignore [misc]
    children = List(
        lambda: CategoryType,
        description=_("categories"),
    )
    image = String(description=_("category image url"))
    markup_percent = Float(required=False, description=_("markup percentage"))
    filterable_attributes = List(
        NonNull(FilterableAttributeType),
        description=_("which attributes and values can be used for filtering this category."),
    )
    min_max_prices = Field(
        NonNull(MinMaxPriceType),
        description=_("minimum and maximum prices for products in this category, if available."),
    )
    tags = DjangoFilterConnectionField(lambda: CategoryTagType, description=_("tags for this category"))
    products = DjangoFilterConnectionField(lambda: ProductType, description=_("products in this category"))
    seo_meta = Field(SEOMetaType, description=_("SEO Meta snapshot"))

    class Meta:
        model = Category
        interfaces = (relay.Node,)
        fields = (
            "uuid",
            "markup_percent",
            "attributes",
            "children",
            "name",
            "slug",
            "description",
            "image",
            "min_max_prices",
        )
        filter_fields = ["uuid"]
        description = _("categories")

    def resolve_children(self, info) -> TreeQuerySet:
        categories = Category.objects.filter(parent=self)
        if info.context.user.has_perm("core.view_category"):
            return categories
        return categories.filter(is_active=True)

    def resolve_image(self: Category, info) -> str:
        return info.context.build_absolute_uri(self.image.url) if self.image else ""

    def resolve_markup_percent(self: Category, info) -> float:
        if info.context.user.has_perm("core.view_category"):
            return float(self.markup_percent)
        return 0.0

    def resolve_filterable_attributes(self: Category, info):
        filterable_results = cache.get(f"{self.uuid}_filterable_results", [])

        if len(filterable_results) > 0:
            return filterable_results

        for attr in (
            self.attributes.all()
            if info.context.user.has_perm("view_attribute")
            else self.attributes.filter(is_active=True)
        ):
            distinct_vals = (
                AttributeValue.objects.annotate(value_length=Length("value"))
                .filter(attribute=attr, attribute__is_filterable=True, product__category=self, value_length__lte=30)
                .values_list("value", flat=True)
                .distinct()
            )

            distinct_vals_list = list(distinct_vals)

            if len(distinct_vals_list) <= 128:
                filterable_results.append(
                    {
                        "attribute_name": attr.name,
                        "possible_values": distinct_vals_list,
                    }
                )
            else:
                pass

        cache.set(f"{self.uuid}_filterable_results", filterable_results, 86400)

        return filterable_results

    def resolve_min_max_prices(self: Category, _info):
        min_max_prices = cache.get(key=f"{self.name}_min_max_prices", default={})

        if not min_max_prices:
            price_aggregation = Product.objects.filter(category=self).aggregate(
                min_price=Min("stocks__price"), max_price=Max("stocks__price")
            )
            min_max_prices["min_price"] = price_aggregation.get("min_price", 0.0)
            min_max_prices["max_price"] = price_aggregation.get("max_price", 0.0)
            cache.set(key=f"{self.name}_min_max_prices", value=min_max_prices, timeout=86400)

        return {
            "min_price": min_max_prices["min_price"],
            "max_price": min_max_prices["max_price"],
        }

    def resolve_seo_meta(self: Category, info):
        lang = graphene_current_lang()
        base = f"https://{config.BASE_DOMAIN}"
        canonical = f"{base}/{lang}/catalog/{self.slug}"
        title = f"{self.name} | {config.PROJECT_NAME}"
        description = (self.description or "")[:180]

        og_image = graphene_abs(info.context, self.image.url) if getattr(self, "image", None) else ""

        og = {
            "title": title,
            "description": description,
            "type": "website",
            "url": canonical,
            "image": og_image,
        }
        tw = {"card": "summary_large_image", "title": title, "description": description, "image": og_image}

        crumbs = [("Home", f"{base}/")]
        for c in self.get_ancestors():
            crumbs.append((c.name, f"{base}/{lang}/catalog/{c.slug}"))
        crumbs.append((self.name, canonical))

        json_ld = [org_schema(), website_schema(), breadcrumb_schema(crumbs), category_schema(self, canonical)]

        product_urls = []
        qs = (
            Product.objects.filter(
                is_active=True,
                category=self,
                brand__is_active=True,
                stocks__vendor__is_active=True,
            )
            .only("slug")
            .distinct()[:24]
        )
        for p in qs:
            product_urls.append(f"{base}/{lang}/product/{p.slug}")
        if product_urls:
            json_ld.append(item_list_schema(product_urls))

        return {
            "title": title,
            "description": description,
            "canonical": canonical,
            "robots": "index,follow",
            "open_graph": og,
            "twitter": tw,
            "json_ld": json_ld,
            "hreflang": info.context.LANGUAGE_CODE,
        }


class VendorType(DjangoObjectType):  # type: ignore [misc]
    markup_percent = Float(description=_("markup percentage"))

    class Meta:
        model = Vendor
        interfaces = (relay.Node,)
        fields = ("uuid", "name", "markup_percent")
        filter_fields = ["uuid"]
        description = _("vendors")


class AddressType(DjangoObjectType):  # type: ignore [misc]
    latitude = Float(description=_("Latitude (Y coordinate)"))
    longitude = Float(description=_("Longitude (X coordinate)"))

    class Meta:
        model = Address
        interfaces = (relay.Node,)
        fields = (
            "uuid",
            "street",
            "district",
            "city",
            "region",
            "postal_code",
            "country",
            "raw_data",
            "api_response",
            "user",
        )
        read_only_fields = ("api_response",)

    def resolve_latitude(self: Address, _info):
        # noinspection PyUnresolvedReferences
        return self.location.x if self.location else None

    def resolve_longitude(self: Address, _info):
        # noinspection PyUnresolvedReferences
        return self.location.y if self.location else None


class FeedbackType(DjangoObjectType):  # type: ignore [misc]
    comment = String(description=_("comment"))
    rating = Int(description=_("rating value from 1 to 10, inclusive, or 0 if not set."))

    class Meta:
        model = Feedback
        interfaces = (relay.Node,)
        fields = ("uuid", "comment", "rating")
        filter_fields = ["uuid"]
        description = _("represents feedback from a user.")


class OrderProductType(DjangoObjectType):  # type: ignore [misc]
    attributes = GenericScalar(description=_("attributes"))
    notifications = GenericScalar(description=_("notifications"))
    download_url = String(description=_("download url for this order product if applicable"))
    feedback = Field(lambda: FeedbackType, description=_("feedback"))

    class Meta:
        model = OrderProduct
        interfaces = (relay.Node,)
        fields = (
            "uuid",
            "product",
            "quantity",
            "status",
            "comments",
            "attributes",
            "notifications",
            "feedback",
        )
        filter_fields = ["uuid"]
        description = _("order products")

    def resolve_feedback(self: OrderProduct, _info):
        feedbacks = Feedback.objects.filter(order_product=self)
        return feedbacks.first() if feedbacks.exists() else None

    def resolve_attributes(self, _info):
        return camelize(self.attributes)

    def resolve_notifications(self, _info):
        return camelize(self.notifications)

    def resolve_download_url(self: OrderProduct, _info) -> str | None:
        return self.download_url


class OrderType(DjangoObjectType):  # type: ignore [misc]
    order_products = DjangoFilterConnectionField(
        OrderProductType, description=_("a list of order products in this order")
    )
    billing_address = Field(AddressType, description=_("billing address"))
    shipping_address = Field(
        AddressType,
        description=_("shipping address for this order, leave blank if same as billing address or if not applicable"),
    )
    total_price = Float(description=_("total price of this order"))
    total_quantity = Int(description=_("total quantity of products in order"))
    is_whole_digital = Float(description=_("are all products in the order digital"))
    attributes = GenericScalar(description=_("attributes"))
    notifications = GenericScalar(description=_("notifications"))
    payments_transactions = Field(TransactionType, description=_("transactions for this order"))

    class Meta:
        model = Order
        interfaces = (relay.Node,)
        fields = (
            "uuid",
            "billing_address",
            "shipping_address",
            "status",
            "promo_code",
            "buy_time",
            "user",
            "total_price",
            "total_quantity",
            "is_whole_digital",
            "human_readable_id",
            "payments_transactions",
        )
        description = _("orders")

    def resolve_total_price(self: Order, _info) -> float:
        return self.total_price

    def resolve_total_quantity(self: Order, _info) -> int:
        return self.total_quantity

    def resolve_notifications(self: Order, _info) -> dict[str, Any]:
        return camelize(self.notifications)

    def resolve_attributes(self: Order, _info) -> dict[str, Any]:
        return camelize(self.attributes)

    def resolve_payments_transactions(self: Order, _info):
        if self.payments_transactions:
            return self.payments_transactions.all()
        return None


class ProductImageType(DjangoObjectType):  # type: ignore [misc]
    image = String(description=_("image url"))

    class Meta:
        model = ProductImage
        interfaces = (relay.Node,)
        fields = ("uuid", "alt", "priority", "image")
        filter_fields = ["uuid"]
        description = _("product's images")

    def resolve_image(self: ProductImage, info):
        return info.context.build_absolute_uri(self.image.url) if self.image else ""


class ProductType(DjangoObjectType):  # type: ignore [misc]
    category = Field(CategoryType, description=_("category"))
    images = DjangoFilterConnectionField(ProductImageType, description=_("images"))
    feedbacks = DjangoFilterConnectionField(FeedbackType, description=_("feedbacks"))
    brand = Field(BrandType, description=_("brand"))
    attribute_groups = DjangoFilterConnectionField(AttributeGroupType, description=_("attribute groups"))
    price = Float(description=_("price"))
    quantity = Float(description=_("quantity"))
    feedbacks_count = Int(description=_("number of feedbacks"))
    personal_orders_only = Boolean(description=_("only available for personal orders"))
    seo_meta = Field(SEOMetaType, description=_("SEO Meta snapshot"))
    rating = Float(description=_("rating value from 1 to 10, inclusive, or 0 if not set."))

    class Meta:
        model = Product
        interfaces = (relay.Node,)
        fields = (
            "uuid",
            "sku",
            "category",
            "brand",
            "tags",
            "name",
            "slug",
            "description",
            "feedbacks",
            "feedbacks_count",
            "personal_orders_only",
            "quantity",
            "attribute_groups",
            "images",
            "price",
            "rating",
        )
        filter_fields = ["uuid", "name"]
        description = _("products")

    def resolve_price(self: Product, _info) -> float:
        return self.price or 0.0

    def resolve_rating(self: Product, _info) -> float:
        return self.rating or 0.0

    def resolve_feedbacks(self: Product, _info):
        if _info.context.user.has_perm("core.view_feedback"):
            return Feedback.objects.filter(order_product__product=self)
        return Feedback.objects.filter(order_product__product=self, is_active=True)

    def resolve_feedbacks_count(self: Product, _info) -> int:
        return self.feedbacks_count or 0

    def resolve_attribute_groups(self: Product, info):
        info.context._product_uuid = self.uuid

        return AttributeGroup.objects.filter(attributes__values__product=self).distinct()

    def resolve_quantity(self: Product, _info) -> int:
        return self.quantity or 0

    def resolve_personal_orders_only(self: Product, _info) -> bool:
        return False or self.personal_orders_only

    def resolve_seo_meta(self: Product, info):
        lang = graphene_current_lang()
        base = f"https://{config.BASE_DOMAIN}"
        canonical = f"{base}/{lang}/product/{self.slug}"
        title = f"{self.name} | {config.PROJECT_NAME}"
        description = (self.description or "")[:180]

        first_img = self.images.order_by("priority").first()
        og_image = graphene_abs(info.context, first_img.image.url) if first_img else ""

        og = {
            "title": title,
            "description": description,
            "type": "product",
            "url": canonical,
            "image": og_image,
        }
        tw = {"card": "summary_large_image", "title": title, "description": description, "image": og_image}

        crumbs = [("Home", f"{base}/")]
        if self.category:
            for c in self.category.get_ancestors(include_self=True):
                crumbs.append((c.name, f"{base}/{lang}/catalog/{c.slug}"))
        crumbs.append((self.name, canonical))

        images = list(self.images.all()[:6])
        rating = {"value": self.rating, "count": self.feedbacks_count}

        json_ld = [
            org_schema(),
            website_schema(),
            breadcrumb_schema(crumbs),
            product_schema(self, images, rating=rating),
        ]

        return {
            "title": title,
            "description": description,
            "canonical": canonical,
            "robots": "index,follow",
            "open_graph": og,
            "twitter": tw,
            "json_ld": json_ld,
            "hreflang": info.context.LANGUAGE_CODE,
        }


class AttributeValueType(DjangoObjectType):  # type: ignore [misc]
    value = String(description=_("attribute value"))

    class Meta:
        model = AttributeValue
        interfaces = (relay.Node,)
        fields = ("uuid", "value")
        filter_fields = ["uuid", "value"]
        description = _("attribute value")


class PromoCodeType(DjangoObjectType):  # type: ignore [misc]
    discount = Float()
    discount_type = String()

    class Meta:
        model = PromoCode
        interfaces = (relay.Node,)
        fields = (
            "uuid",
            "code",
            "start_time",
            "end_time",
            "used_on",
        )
        filter_fields = ["uuid"]
        description = _("promocodes")

    def resolve_discount(self: PromoCode, _info) -> float:
        return float(self.discount_percent) if self.discount_percent else float(self.discount_amount)  # type: ignore [arg-type]

    def resolve_discount_type(self: PromoCode, _info) -> str:
        return "percent" if self.discount_percent else "amount"


class PromotionType(DjangoObjectType):  # type: ignore [misc]
    products = DjangoFilterConnectionField(ProductType, description=_("products on sale"))

    class Meta:
        model = Promotion
        interfaces = (relay.Node,)
        fields = ("uuid", "name", "discount_percent", "products")
        filter_fields = ["uuid"]
        description = _("promotions")


class StockType(DjangoObjectType):  # type: ignore [misc]
    vendor = Field(VendorType, description=_("vendor"))
    product = Field(ProductType, description=_("product"))

    class Meta:
        model = Stock
        interfaces = (relay.Node,)
        fields = ("uuid", "vendor", "product", "price", "quantity", "sku")
        filter_fields = ["uuid"]
        description = _("stocks")


class WishlistType(DjangoObjectType):  # type: ignore [misc]
    products = DjangoFilterConnectionField(ProductType, description=_("wishlisted products"))

    class Meta:
        model = Wishlist
        interfaces = (relay.Node,)
        fields = ("uuid", "products", "user")
        description = _("wishlists")


class ProductTagType(DjangoObjectType):  # type: ignore [misc]
    product_set = DjangoFilterConnectionField(ProductType, description=_("tagged products"))

    class Meta:
        model = ProductTag
        interfaces = (relay.Node,)
        fields = ("uuid", "tag_name", "name", "product_set")
        filter_fields = ["uuid", "tag_name", "name"]
        description = _("product tags")


class CategoryTagType(DjangoObjectType):  # type: ignore [misc]
    category_set = DjangoFilterConnectionField(CategoryType, description=_("tagged categories"))

    class Meta:
        model = CategoryTag
        interfaces = (relay.Node,)
        fields = ("uuid", "tag_name", "name", "category_set")
        filter_fields = ["uuid", "tag_name", "name"]
        description = _("categories tags")


class ConfigType(ObjectType):  # type: ignore [misc]
    project_name = String(description=_("project name"))
    base_domain = String(description=_("company email"))
    company_name = String(description=_("company name"))
    company_address = String(description=_("company address"))
    company_phone_number = String(description=_("company phone number"))
    email_from = String(description=_("email from, sometimes it must be used instead of host user value"))
    email_host_user = String(description=_("email host user"))
    payment_gateway_maximum = Float(description=_("maximum amount for payment"))
    payment_gateway_minimum = Float(description=_("minimum amount for payment"))
    analytics_data = GenericScalar(description=_("analytics data"))
    advertisement_data = GenericScalar(description=_("advertisement data"))

    class Meta:
        description = _("company configuration")


class LanguageType(ObjectType):  # type: ignore [misc]
    code = String(description=_("language code"))
    name = String(description=_("language name"))
    flag = String(description=_("language flag, if exists :)"))

    class Meta:
        description = _("supported languages")


class SearchProductsResultsType(ObjectType):  # type: ignore [misc]
    uuid = UUID()
    name = String()
    slug = String()
    image = String()


class SearchCategoriesResultsType(ObjectType):  # type: ignore [misc]
    uuid = UUID()
    name = String()
    slug = String()
    image = String()


class SearchBrandsResultsType(ObjectType):  # type: ignore [misc]
    uuid = UUID()
    name = String()
    slug = String()
    image = String()


class SearchPostsResultsType(ObjectType):  # type: ignore [misc]
    uuid = UUID()
    name = String()
    slug = String()


class SearchResultsType(ObjectType):  # type: ignore [misc]
    products = List(description=_("products search results"), of_type=SearchProductsResultsType)
    categories = List(description=_("products search results"), of_type=SearchCategoriesResultsType)
    brands = List(description=_("products search results"), of_type=SearchBrandsResultsType)
    posts = List(description=_("posts search results"), of_type=SearchPostsResultsType)


class BulkProductInput(InputObjectType):  # type: ignore [misc]
    uuid = UUID(required=True)
    attributes = GenericScalar(required=False)
