import os
from decimal import Decimal

import lxml.html
from django.db import models
from django.db.models.functions import Coalesce
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.functional import cached_property
from django.utils.translation import gettext_lazy as _

from oscar.apps.catalogue import abstract_models as oscar
from oscar.core.loading import get_class
from oscar.models.fields import AutoSlugField

from ocyan.plugin.oscar.fender import ALL_TAX_RATES
from ocyan.plugin.oscar.fender import get_default_tax_rate as vat_default

from ocyan.plugin.oscar_catalogue.constants import (
    DEFAULT_SLUG,
    MINIMIM_QUANTITY_ATTRIBUTE,
)

tax_rate_as_fraction = get_class("taxes.taxrate", "tax_rate_as_fraction")
AbstractSEOFields = get_class("catalogue.helpers", "AbstractSEOFields")


class AbstractProductClass(oscar.AbstractProductClass):
    slug = AutoSlugField(
        _("Slug"), max_length=128, unique=True, populate_from="name", editable=True
    )

    class Meta(oscar.AbstractProductClass.Meta):
        abstract = True


class AbstractOption(oscar.AbstractOption):
    code = AutoSlugField(
        _("Code"), max_length=128, unique=True, populate_from="name", editable=True
    )
    order = models.IntegerField(
        _("Ordering"),
        null=True,
        blank=True,
        help_text=_("Controls the ordering of product options on product detail pages"),
        db_index=True,
    )

    class Meta(oscar.AbstractOption.Meta):
        abstract = True
        ordering = ["order", "name"]


class AbstractCategory(oscar.AbstractCategory, AbstractSEOFields):
    class Meta(oscar.AbstractCategory.Meta):
        abstract = True


class AbstractProductAttribute(oscar.AbstractProductAttribute):
    EMBED = "embed"

    TYPE_CHOICES = oscar.AbstractProductAttribute.TYPE_CHOICES + ((EMBED, _("Embed")),)

    type = models.CharField(
        choices=TYPE_CHOICES,
        default=TYPE_CHOICES[0][0],
        max_length=20,
        verbose_name=_("Type"),
    )

    class Meta(oscar.AbstractProductAttribute.Meta):
        abstract = True

    # pylint: disable=protected-access
    _validate_embed = oscar.AbstractProductAttribute._validate_text


class AbstractProductAttributeValue(oscar.AbstractProductAttributeValue):
    value_embed = models.TextField(
        _("Youtube Video ID"),
        blank=True,
        null=True,
        help_text=_(
            "This can be found in the URL of a video. So for example: https://www.youtube.com/watch?v=dkIU-ygvtps. In this case {dkIU-ygvtps} is the Youtube Video ID"
        ),
    )

    class Meta(oscar.AbstractProductAttributeValue.Meta):
        abstract = True

    @property
    def _embed_as_html(self):
        return render_to_string(
            "oscar/catalogue/partials/embed.html", {"value_embed": self.value}
        )

    @property
    def _file_as_html(self):
        return render_to_string(
            "oscar/catalogue/partials/file.html",
            {"value_file": self.value, "display_name": self._file_as_text},
        )

    @property
    def _image_as_html(self):
        return render_to_string(
            "oscar/catalogue/partials/image.html",
            {"value_image": self.value, "display_name": self._image_as_text},
        )

    @property
    def _file_as_text(self):
        return os.path.basename(self.value.name)

    @property
    def _image_as_text(self):
        return os.path.basename(self.value.name)


class AbstractProduct(oscar.AbstractProduct, AbstractSEOFields):
    vat_rate = models.CharField(
        _("VAT rate"),
        max_length=5,
        choices=ALL_TAX_RATES,
        default=vat_default(),
        null=True,
    )

    long_description = models.TextField(
        _("Content"),
        blank=True,
        null=True,
        help_text=_("A far more elaborate, but optional description of the product"),
    )

    @property
    def min_quantity(self):
        min_quantity_values = self.get_attribute_values().filter(
            attribute__code=MINIMIM_QUANTITY_ATTRIBUTE
        )
        if min_quantity_values.exists():
            return min_quantity_values.first().value

        return 1

    @property
    def tax_percentage(self):
        return tax_rate_as_fraction(self.vat_rate)

    @property
    def variants_ordered_by_price(self):
        return (
            self.children.filter(is_public=True)
            .annotate(
                lowest_price=Coalesce(
                    models.Avg("stockrecords__price_excl_tax"),
                    models.Value(
                        Decimal(999999999)
                    ),  # put children with no stock at the end
                )
            )
            .order_by("lowest_price", "-priority")
        )

    @property
    def variants_ordered_for_admin(self):
        return (
            self.children.all()
            .annotate(
                lowest_price=Coalesce(
                    models.Avg("stockrecords__price_excl_tax"),
                    models.Value(
                        Decimal(999999999)
                    ),  # put children with no stock at the end
                )
            )
            .order_by("lowest_price", "-priority")
        )

    @property
    def options(self):
        """
        TODO: Is this really needed????
        Override to give precedence to options defined on product over product_class
        """
        if self.product_options.exists():
            return self.product_options.distinct()

        return self.get_product_class().options.distinct()

    @cached_property
    def has_options(self):
        # Extracting annotated value with number of product class options
        # from product list queryset.
        has_product_class_options = getattr(self, "has_product_class_options", None)
        has_product_options = getattr(self, "has_product_options", None)
        if has_product_class_options is not None and has_product_options is not None:
            return has_product_class_options or has_product_options
        # this can be changed to self.options.exists() if we revert to the default implementation.
        return (
            self.get_product_class().options.all() | self.product_options.all()
        ).exists()

    @property
    def sorted_recommended_products(self):
        """Keeping order by recommendation ranking."""
        return [
            r.recommendation
            for r in self.primary_recommendations.filter(recommendation__is_public=True)
            .select_related("recommendation")
            .all()
        ]

    def primary_category(self):
        return self.categories.first()

    def get_absolute_url(self):
        slug = self.slug or DEFAULT_SLUG
        return reverse("catalogue:detail", kwargs={"product_slug": slug, "pk": self.pk})

    class Meta(oscar.AbstractProduct.Meta):
        abstract = True
        ordering = ["-priority", "-date_created"]

    def clean(self):
        super().clean()

        # We'll clean out html fields, so they don't have an empty p or whatever.
        # This allows us to use just check for product.description et al.
        for name in ["description", "long_description"]:
            value = getattr(self, name) or ""
            value = value.strip()  # pylint: disable=no-member
            if "<img" in value:
                continue
            if value:
                tree = lxml.html.fromstring(value)
                if not tree.text_content().strip():
                    value = ""
            setattr(self, name, value)


class AbstractEntityAttributeValue(models.Model):
    """
    This class can be used to implement entities that can be picked in
    the dashboard and still work in oscarapi and the import export plugin.
    """

    name = models.CharField(_("Name"), max_length=125)
    code = models.SlugField(_("Code"), max_length=140, unique=True)

    def __str__(self):
        return self.name

    def json(self):
        return self.code

    class Meta:
        abstract = True
