from __future__ import annotations

import os
import uuid
from decimal import Decimal

from django.conf import settings
from django.contrib.postgres.indexes import BTreeIndex
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
from django.db import models
from django.db.models import (
    Avg,
    F,
)
from django.utils.safestring import SafeString, mark_safe
from django.utils.translation import gettext_lazy as _
from django_stubs_ext.db.models import TypedModelMeta
from djmoney.models.fields import MoneyField
from djmoney.money import Money
from measurement.measures import Weight
from mptt.fields import TreeForeignKey
from parler.fields import TranslationsForeignKey
from parler.models import TranslatableModel, TranslatedFieldsModel
from simple_history.models import HistoricalRecords
from tinymce.models import HTMLField

from core.fields.measurement import MeasurementField
from core.models import (
    MetaDataModel,
    SoftDeleteModel,
    TimeStampMixinModel,
    UUIDModel,
)
from core.units import WeightUnits
from core.utils.generators import SlugifyConfig, unique_slugify
from core.weight import zero_weight
from meili.models import IndexMixin
from product.managers.product import ProductManager
from product.models.image import ProductImage
from core.models import SeoModel
from tag.models.tagged_item import TaggedModel

DISCOUNT_PERCENT_MIN = Decimal("0.0")
DISCOUNT_PERCENT_MAX = Decimal("100.0")


class Product(
    SoftDeleteModel,
    TranslatableModel,
    TimeStampMixinModel,
    SeoModel,
    UUIDModel,
    MetaDataModel,
    TaggedModel,
):
    id = models.BigAutoField(primary_key=True)
    sku = models.CharField(
        _("SKU"), unique=True, max_length=100, default=uuid.uuid4
    )
    category = TreeForeignKey(
        "product.ProductCategory",
        on_delete=models.SET_NULL,
        related_name="products",
        null=True,
        blank=True,
    )
    slug = models.SlugField(_("Slug"), max_length=255, unique=True)
    price = MoneyField(
        _("Price"),
        max_digits=11,
        decimal_places=2,
        default=0,
    )
    active = models.BooleanField(_("Active"), default=True)
    stock = models.PositiveIntegerField(_("Stock"), default=0)
    discount_percent = models.DecimalField(
        _("Discount Percent"),
        max_digits=11,
        decimal_places=2,
        default=Decimal("0.0"),
    )
    vat = models.ForeignKey(
        "vat.Vat",
        related_name="products",
        blank=True,
        null=True,
        on_delete=models.SET_NULL,
    )
    view_count = models.PositiveBigIntegerField(_("View Count"), default=0)
    weight = MeasurementField(
        str(_("Weight")),
        measurement=Weight,
        unit_choices=WeightUnits.CHOICES,
        default=zero_weight,
    )
    changed_by = models.ForeignKey(
        "user.UserAccount",
        on_delete=models.SET_NULL,
        related_name="changed_products",
        null=True,
        blank=True,
    )
    history = HistoricalRecords()

    points_coefficient = models.DecimalField(
        _("Points Coefficient"),
        max_digits=5,
        decimal_places=2,
        default=Decimal("1.0"),
        validators=[MinValueValidator(Decimal("0.0"))],
        help_text=_(
            "Multiplier for loyalty points calculation (0 = no calculated points)"
        ),
    )
    points = models.PositiveIntegerField(
        _("Bonus Points"),
        default=0,
        help_text=_("Fixed bonus points awarded on purchase"),
    )

    objects: ProductManager = ProductManager()

    class Meta(MetaDataModel.Meta, TypedModelMeta):
        verbose_name = _("Product")
        verbose_name_plural = _("Products")
        ordering = ["-created_at"]
        indexes = [
            *MetaDataModel.Meta.indexes,
            *TimeStampMixinModel.Meta.indexes,
            BTreeIndex(
                fields=["price", "stock"], name="product_price_stock_ix"
            ),
            BTreeIndex(fields=["sku"], name="product_sku_ix"),
            BTreeIndex(fields=["slug"], name="product_slug_ix"),
            BTreeIndex(fields=["price"], name="product_price_ix"),
            BTreeIndex(fields=["stock"], name="product_stock_ix"),
            BTreeIndex(fields=["discount_percent"], name="product_discount_ix"),
            BTreeIndex(fields=["view_count"], name="product_view_count_ix"),
            BTreeIndex(fields=["weight"], name="product_weight_ix"),
            BTreeIndex(fields=["active"], name="product_active_ix"),
            BTreeIndex(fields=["category"], name="product_category_ix"),
            BTreeIndex(
                fields=["active", "price"], name="product_active_price_ix"
            ),
            BTreeIndex(
                fields=["points_coefficient"], name="product_points_coeff_ix"
            ),
        ]

    def __str__(self):
        return self.safe_translation_getter("name", any_language=True) or ""

    def __repr__(self):
        name = self.safe_translation_getter("name", any_language=True) or ""
        return f"<Product: {name} ({self.sku})>"

    def save(self, *args, **kwargs):
        if not self.sku:
            self.sku = self.generate_unique_sku()

        if not self.slug:
            config = SlugifyConfig(
                instance=self,
                title_field="name",
            )
            self.slug = unique_slugify(config)
        super().save(*args, **kwargs)

    def clean(self):
        super().clean()
        if self.discount_percent > 0 >= self.price.amount:
            raise ValidationError(
                {
                    "discount_percent": _(
                        "Discount percent cannot be greater than 0 if price is 0."
                    )
                }
            )

        if (
            not DISCOUNT_PERCENT_MIN
            <= self.discount_percent
            <= DISCOUNT_PERCENT_MAX
        ):
            raise ValidationError(
                {
                    "discount_percent": _(
                        "Discount percent must be between 0 and 100."
                    )
                }
            )

        if self.stock < 0:
            raise ValidationError({"stock": _("Stock cannot be negative.")})

    def generate_unique_sku(self) -> str:
        while True:
            unique_code = uuid.uuid4()
            if not self.objects.filter(sku=unique_code).exists():
                return str(unique_code)

    def increment_stock(self, quantity: int) -> None:
        if quantity < 0:
            raise ValueError("Quantity to increment must be non-negative")
        Product.objects.filter(id=self.id).update(stock=F("stock") + quantity)
        self.refresh_from_db()

    def decrement_stock(self, quantity: int) -> None:
        if quantity < 0:
            raise ValueError("Invalid quantity to decrement")
        updated_rows = Product.objects.filter(
            id=self.id, stock__gte=quantity
        ).update(stock=F("stock") - quantity)
        if not updated_rows:
            raise ValueError("Not enough stock to decrement")
        self.refresh_from_db()

    @property
    def _history_user(self):
        return self.changed_by

    @_history_user.setter
    def _history_user(self, value):
        self.changed_by = value

    @property
    def discount_value(self) -> Money:
        value = (self.price.amount * self.discount_percent) / 100
        return Money(value, settings.DEFAULT_CURRENCY)

    @property
    def price_save_percent(self) -> Decimal:
        if self.price.amount > 0:
            return Decimal(
                (self.discount_value.amount / self.price.amount) * 100
            )
        return Decimal(0)

    @property
    def likes_count(self) -> int:
        """Return the number of likes/favourites for this product."""
        # If annotation exists, use it
        if "likes_count" in self.__dict__:
            return self.__dict__["likes_count"]
        # Otherwise query the database
        return self.favourited_by.count()

    @likes_count.setter
    def likes_count(self, value):
        """Allow Django to set the annotation value."""
        self.__dict__["likes_count"] = value

    @property
    def review_average(self) -> float:
        """Return the average review rating for this product."""
        # If annotation exists, use it
        if "review_average" in self.__dict__:
            cached_value = self.__dict__["review_average"]
            return float(cached_value) if cached_value is not None else 0.0
        # Otherwise query the database
        avg = self.reviews.aggregate(avg=Avg("rate"))["avg"]
        return float(avg) if avg is not None else 0.0

    @review_average.setter
    def review_average(self, value):
        """Allow Django to set the annotation value."""
        self.__dict__["review_average"] = value

    @property
    def review_count(self) -> int:
        """Return the number of reviews for this product."""
        # If annotation exists (stored as reviews_count), use it
        if "reviews_count" in self.__dict__:
            return self.__dict__["reviews_count"]
        # Otherwise query the database
        return self.reviews.count()

    @review_count.setter
    def review_count(self, value):
        """Allow Django to set the annotation value."""
        self.__dict__["reviews_count"] = value

    @property
    def vat_percent(self) -> Decimal:
        if self.vat:
            return self.vat.value
        return Decimal(0)

    @property
    def vat_value(self) -> Money:
        if self.vat:
            value = (self.price.amount * self.vat.value) / 100
            return Money(value, settings.DEFAULT_CURRENCY)
        return Money(0, settings.DEFAULT_CURRENCY)

    @property
    def final_price(self) -> Money:
        price_currency = self.price.currency
        vat_value = (
            Money(self.vat_value.amount, price_currency)
            if self.vat_value.currency != price_currency
            else self.vat_value
        )
        discount_value = (
            Money(self.discount_value.amount, price_currency)
            if self.discount_value.currency != price_currency
            else self.discount_value
        )
        return self.price + vat_value - discount_value

    @property
    def main_image_path(self) -> str:
        # Use prefetched main images if available (from for_list() queryset)
        # to avoid an N+1 query per product
        prefetched = getattr(self, "_prefetched_main_images", None)
        if prefetched is not None:
            product_image = prefetched[0] if prefetched else None
        else:
            product_image = ProductImage.objects.filter(
                product_id=self.id, is_main=True
            ).first()
        if not product_image:
            return ""
        return f"media/uploads/products/{os.path.basename(product_image.image.name)}"

    @property
    def colored_stock(self) -> SafeString:
        if self.stock > 0:
            return mark_safe(
                '<span style="color: #1bff00;">{}</span>'.format(self.stock)
            )
        else:
            return mark_safe(
                '<span style="color: #ff0000;">{}</span>'.format(self.stock)
            )


class ProductTranslation(TranslatedFieldsModel, IndexMixin):
    master = TranslationsForeignKey(
        "product.Product",
        on_delete=models.CASCADE,
        related_name="translations",
        null=True,
    )
    name = models.CharField(_("Name"), max_length=255, blank=True, default="")
    description = HTMLField(_("Description"), blank=True, null=True)

    class Meta:
        app_label = "product"
        db_table = "product_product_translation"
        unique_together = ("language_code", "master")
        verbose_name = _("Product Translation")
        verbose_name_plural = _("Product Translations")

    @classmethod
    def get_meilisearch_queryset(cls):
        """Return optimized queryset for bulk indexing."""
        from django.db.models import Count

        return cls.objects.select_related(
            "master", "master__category", "master__vat"
        ).annotate(
            _likes_count=Count("master__favourited_by", distinct=True),
            _review_average=Avg("master__reviews__rate"),
            _reviews_count=Count("master__reviews", distinct=True),
        )

    def meili_filter(self) -> bool:
        """
        Determine if this translation should be indexed.

        Only index translations for active, non-deleted products.
        """
        if not self.master_id:
            return False
        try:
            return self.master.active and not self.master.is_deleted
        except Product.DoesNotExist:
            return False

    class MeiliMeta:
        filterable_fields = (
            "name",
            "language_code",
            "likes_count",
            "final_price",
            "view_count",
            "category",
            "category_name",
            "stock",
            "active",
            "is_deleted",
            "master_id",
            "attributes",
            "attribute_values",
        )
        searchable_fields = (
            "master_id",
            "name",
            "description",
            "attribute_names",
            "attribute_values_text",
        )
        displayed_fields = (
            "id",
            "master_id",
            "name",
            "description",
            "language_code",
            "likes_count",
            "final_price",
            "view_count",
            "category",
            "category_name",
            "stock",
            "discount_percent",
            "active",
            "is_deleted",
            "attributes",
            "attribute_data",
        )
        sortable_fields = (
            "likes_count",
            "final_price",
            "view_count",
            "discount_percent",
            "created_at",
            "stock",
        )
        ranking_rules = [
            "words",
            "typo",
            "proximity",
            "attribute",
            "sort",
            "exactness",
            "stock:desc",
            "discount_percent:desc",
        ]
        synonyms = {
            # English synonyms
            "laptop": ["notebook", "portable computer"],
            "notebook": ["laptop", "portable computer"],
            "phone": ["mobile", "smartphone", "cellphone"],
            "mobile": ["phone", "smartphone", "cellphone"],
            "smartphone": ["phone", "mobile", "cellphone", "handy"],
            # Greek synonyms
            "υπολογιστής": ["κομπιούτερ", "pc"],
            "κομπιούτερ": ["υπολογιστής", "pc"],
            "τηλέφωνο": ["κινητό"],
            "κινητό": ["τηλέφωνο"],
            # German synonyms
            "handy": ["smartphone", "mobiltelefon"],
            "mobiltelefon": ["handy", "smartphone"],
            "computer": ["rechner", "pc"],
            "rechner": ["computer", "pc"],
        }
        typo_tolerance = {
            "enabled": True,
            "minWordSizeForTypos": {"oneTypo": 3, "twoTypos": 5},
            "disableOnWords": [],
            "disableOnAttributes": [],
        }
        faceting = {"maxValuesPerFacet": 100}
        pagination = {"maxTotalHits": 5_000_000}
        search_cutoff_ms = 1500

    @classmethod
    def get_additional_meili_fields(cls):
        return {
            "master_id": lambda obj: obj.master_id,
            "likes_count": lambda obj: (
                getattr(obj, "_likes_count", 0) or obj.master.likes_count
            ),
            "view_count": lambda obj: obj.master.view_count,
            "final_price": lambda obj: float(obj.master.final_price.amount),
            "discount_percent": lambda obj: float(obj.master.discount_percent),
            "created_at": lambda obj: (
                obj.master.created_at.isoformat()
                if obj.master.created_at
                else None
            ),
            "category": lambda obj: obj.master.category_id,
            "category_name": lambda obj: (
                obj.master.category.safe_translation_getter(
                    "name", any_language=True
                )
                if obj.master.category
                else None
            ),
            "stock": lambda obj: obj.master.stock,
            "active": lambda obj: obj.master.active,
            "is_deleted": lambda obj: obj.master.is_deleted,
            "attributes": lambda obj: list(
                obj.master.product_attributes.values_list(
                    "attribute_value__attribute_id", flat=True
                ).distinct()
            ),
            "attribute_values": lambda obj: list(
                obj.master.product_attributes.values_list(
                    "attribute_value_id", flat=True
                )
            ),
            "attribute_names": lambda obj: " ".join(
                [
                    pa.attribute_value.attribute.safe_translation_getter(
                        "name",
                        language_code=obj.language_code,
                        any_language=True,
                    )
                    for pa in obj.master.product_attributes.select_related(
                        "attribute_value__attribute"
                    ).prefetch_related(
                        "attribute_value__attribute__translations"
                    )
                ]
            ),
            "attribute_values_text": lambda obj: " ".join(
                [
                    pa.attribute_value.safe_translation_getter(
                        "value",
                        language_code=obj.language_code,
                        any_language=True,
                    )
                    for pa in obj.master.product_attributes.select_related(
                        "attribute_value"
                    ).prefetch_related("attribute_value__translations")
                ]
            ),
            "attribute_data": lambda obj: [
                {
                    "attribute_id": pa.attribute_value.attribute_id,
                    "attribute_name": pa.attribute_value.attribute.safe_translation_getter(
                        "name",
                        language_code=obj.language_code,
                        any_language=True,
                    ),
                    "value_id": pa.attribute_value_id,
                    "value": pa.attribute_value.safe_translation_getter(
                        "value",
                        language_code=obj.language_code,
                        any_language=True,
                    ),
                }
                for pa in obj.master.product_attributes.select_related(
                    "attribute_value__attribute"
                ).prefetch_related(
                    "attribute_value__translations",
                    "attribute_value__attribute__translations",
                )
            ],
        }

    def __str__(self):
        name = self.name or "Untitled"
        return f"{name} ({self.language_code})"
