# -*- coding: utf-8 -*-
from django.db import models, router
from django.db.models import F, signals
from django.db.models.functions import Coalesce, Least
from django.utils.translation import gettext_lazy as _

from oscar.apps.partner import abstract_models as oscar
from oscar.apps.partner.exceptions import InvalidStockAdjustment


class AbstractStockRecord(oscar.AbstractStockRecord):
    num_in_stock = models.IntegerField(_("Number in stock"), blank=True, null=True)

    @property
    def price_incl_tax(self):
        """
        Since oscar is about to move to specifying price without indicating
        incl or excl tax levels, we are now using the recommended way of
        letting users specify prices including tax and having the strategy
        calculate the tax price. Which is by storing the price incl tax
        in the price excl tax field.
        """
        return self.price_excl_tax

    @price_incl_tax.setter
    def price_incl_tax(self, value):
        self.price_excl_tax = value

    def allocate(self, quantity):
        """
        Record a stock allocation.
        This normally happens when a product is bought at checkout.  When the
        product is actually shipped, then we 'consume' the allocation.
        """
        # Doesn't make sense to allocate if stock tracking is off.
        if not self.can_track_allocations:
            return

        # Send the pre-save signal
        self.pre_save_signal()

        # Atomic update
        (
            self.__class__.objects.filter(pk=self.pk).update(
                num_allocated=(Coalesce(F("num_allocated"), 0) + quantity)
            )
        )

        # Make sure the current object is up-to-date
        self.refresh_from_db(fields=["num_allocated"])

        # Send the post-save signal
        self.post_save_signal()

    def consume_allocation(self, quantity):
        """
        Consume a previous allocation
        This is used when an item is shipped.  We remove the original
        allocation and adjust the number in stock accordingly
        """
        if not self.can_track_allocations:
            return
        if not self.is_allocation_consumption_possible(quantity):
            raise InvalidStockAdjustment(_("Invalid stock consumption request"))

        # send the pre save signal
        self.pre_save_signal()

        # Atomically consume allocations and stock
        (
            self.__class__.objects.filter(pk=self.pk).update(
                num_allocated=(Coalesce(F("num_allocated"), 0) - quantity),
                num_in_stock=(Coalesce(F("num_in_stock"), 0) - quantity),
            )
        )

        # Make sure current object is up-to-date
        self.refresh_from_db(fields=["num_allocated", "num_in_stock"])

        # Send the post-save signal
        self.post_save_signal()

    def cancel_allocation(self, quantity):
        if not self.can_track_allocations:
            return

        # send the pre save signal
        self.pre_save_signal()

        # Atomically consume allocations
        (
            self.__class__.objects.filter(pk=self.pk).update(
                num_allocated=Coalesce(F("num_allocated"), 0)
                - Least(Coalesce(F("num_allocated"), 0), quantity),
            )
        )

        # Make sure current object is up-to-date
        self.refresh_from_db(fields=["num_allocated"])

        # Send the post-save signal
        self.post_save_signal()

    def pre_save_signal(self):
        signals.pre_save.send(
            sender=self.__class__,
            instance=self,
            created=False,
            raw=False,
            using=router.db_for_write(self.__class__, instance=self),
        )

    def post_save_signal(self):
        signals.post_save.send(
            sender=self.__class__,
            instance=self,
            created=False,
            raw=False,
            using=router.db_for_write(self.__class__, instance=self),
        )

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