from decimal import Decimal as D
from decimal import InvalidOperation

from django.db import models
from django.utils.translation import gettext_lazy as _

from oscar.apps.shipping import abstract_models as oscar
from oscar.core.loading import get_class, get_model
from oscar.core.prices import Price

from ocyan.core.fender import config
from ocyan.core.utils import to_money

taxed_shipping_charge = get_class("shipping.utils", "taxed_shipping_charge")


class AbstractOrderAndItemCharges(oscar.AbstractOrderAndItemCharges):
    def calculate(self, basket):
        price = super(AbstractOrderAndItemCharges, self).calculate(basket)
        return taxed_shipping_charge(price, basket.owner, basket.strategy.request)

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


class AbstractWeightBased(oscar.AbstractWeightBased):
    free_shipping_threshold = models.DecimalField(
        _("Free Shipping"), decimal_places=2, max_digits=12, blank=True, null=True
    )

    def calculate(self, basket):
        if (
            self.free_shipping_threshold is not None
            and basket.total_incl_tax >= self.free_shipping_threshold
        ):
            return Price(
                currency=basket.currency, excl_tax=D("0.00"), incl_tax=D("0.00")
            )

        price = super(AbstractWeightBased, self).calculate(basket)
        return taxed_shipping_charge(price, basket.owner, basket.strategy.request)

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


class AbstractProductBased(oscar.AbstractBase):
    free_shipping_threshold = models.DecimalField(
        _("Free Shipping"), decimal_places=2, max_digits=12, blank=True, null=True
    )
    use_highest_shipping_cost = models.BooleanField(
        _("Use highest product shipping cost"),
        default=True,
        help_text=_(
            "If this option is checked, it will use the highest price from the products that are in the users basket, otherwise it will use the lowest price."
        ),
    )
    is_only_shipping_method = models.BooleanField(
        _("Is only shipping method"),
        default=False,
        help_text=_(
            "If the users basket has a product with the shipping_cost attribute set, should it only show this shipping method and hide others?"
        ),
    )

    def calculate(self, basket):
        if (
            self.free_shipping_threshold is not None
            and basket.total_incl_tax >= self.free_shipping_threshold
        ):
            return Price(
                currency=basket.currency, excl_tax=D("0.00"), incl_tax=D("0.00")
            )

        shipping_cost_attribute = config.get(
            "shipping", "shipping_cost_attribute", "shipping_cost"
        )

        # We're in abstract_models, meaning we can't use get_model above this class as the model
        # most likely won't be registered yet (as abstract models aren't).
        Product = get_model("catalogue", "Product")

        products = Product.objects.prefetch_attribute_values().filter(
            pk__in=basket.lines.values_list("product__pk", flat=True)
        )

        product_shipping_prices = []
        for product in products:
            shipping_cost = getattr(product.attr, shipping_cost_attribute, None)
            if shipping_cost is not None:
                try:
                    product_shipping_prices.append(D(shipping_cost))
                except (InvalidOperation, TypeError, ValueError):
                    continue

        if not product_shipping_prices:
            return None

        if self.use_highest_shipping_cost:
            product_shipping_price = to_money(D(max(product_shipping_prices)))
            shipping_price = Price(
                basket.currency, product_shipping_price, product_shipping_price
            )
        else:
            product_shipping_price = to_money(D(min(product_shipping_prices)))
            shipping_price = Price(
                basket.currency, product_shipping_price, product_shipping_price
            )

        return taxed_shipping_charge(
            shipping_price, basket.owner, basket.strategy.request
        )

    class Meta(oscar.AbstractBase.Meta):
        abstract = True
        verbose_name = _("Product attribut based shipping method")
        verbose_name_plural = _("Product attribute based shipping methods")
