import math
import posixpath
import random
import string
from datetime import timedelta
from decimal import Decimal
from io import BytesIO
from typing import Optional

from django.conf import settings
from django.core.cache import cache
from django.core.files.base import ContentFile
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models
from django.db.models import F, Case, When, DecimalField, Prefetch, Max, Q, Subquery, OuterRef
from django.db.models.functions import Ceil, Coalesce
from django.db.models.lookups import GreaterThan
from django.db.transaction import atomic
from django.utils import timezone
from django_cleanup import cleanup
from mptt.fields import TreeForeignKey
from mptt.models import MPTTModel

from store.utils import create_preview


class GlobalSettings(models.Model):
    # currency
    yuan_rate = models.DecimalField('Курс CNY/RUB', max_digits=10, decimal_places=2, default=0)
    yuan_rate_last_updated = models.DateTimeField('Дата обновления курса CNY/RUB', null=True, default=None)
    yuan_rate_commission = models.DecimalField('Наценка на курс юаня, руб', max_digits=10, decimal_places=2, default=0)
    # Chinadelivery
    delivery_price_CN = models.DecimalField('Цена доставки по Китаю', max_digits=10, decimal_places=2, default=0)
    commission_rub = models.DecimalField('Комиссия, руб', max_digits=10, decimal_places=2, default=0)
    pickup_address = models.CharField('Адрес пункта самовывоза', max_length=200, blank=True, null=True)
    time_to_buy = models.DurationField('Время на покупку',
                                       help_text="Через N времени заказ переходит из статуса 'Новый' в 'Черновик'",
                                       default=timedelta(hours=3))

    class Meta:
        verbose_name = 'Глобальные настройки'
        verbose_name_plural = 'Глобальные настройки'

    def save(self, *args, **kwargs):
        # Store only one instance of GlobalSettings
        self.id = 1
        self.__class__.objects.exclude(id=self.id).delete()
        super().save(*args, **kwargs)
        cache.set('global_settings', self)

    def __str__(self) -> str:
        return f'GlobalSettings <{self.id}>'

    @classmethod
    def load(cls) -> 'GlobalSettings':
        obj = cache.get('global_settings')

        if not obj:
            obj, _ = cls.objects.get_or_create(id=1)
            cache.set('global_settings', obj)
        return obj

    @property
    def full_yuan_rate(self):
        return self.yuan_rate + self.yuan_rate_commission


class Category(MPTTModel):
    name = models.CharField('Название', max_length=20)
    parent = TreeForeignKey('self', verbose_name='Родительская категория', on_delete=models.SET_NULL, blank=True, null=True, related_name='children', db_index=True)
    delivery_price_CN_RU = models.DecimalField('Цена доставки Китай-РФ', max_digits=10, decimal_places=2, default=0)  # Chinadelivery2
    commission = models.DecimalField('Дополнительная комиссия, %',
                                     max_digits=10, decimal_places=2, default=0,
                                     validators=[MinValueValidator(0), MaxValueValidator(100)])

    def __str__(self):
        return self.name

    class MPTTMeta:
        order_insertion_by = ['id']
        parent_attr = 'parent'

    class Meta:
        verbose_name = 'Категория'
        verbose_name_plural = 'Категории'

    @property
    def delivery_price(self):
        if not self.delivery_price_CN_RU and self.parent_id:
            return self.parent.delivery_price
        else:
            return self.delivery_price_CN_RU

    @property
    def commission_price(self):
        """ Get commission from object or from its parent """
        if not self.commission and self.parent_id:
            return self.parent.commission_price
        else:
            return self.commission


class PromocodeQuerySet(models.QuerySet):
    def active(self):
        return self.filter(is_active=True)


class Promocode(models.Model):
    name = models.CharField('Название', max_length=100, unique=True)
    discount = models.PositiveIntegerField('Скидка в рублях')
    free_delivery = models.BooleanField('Бесплатная доставка', default=False)  # freedelivery
    no_comission = models.BooleanField('Без комиссии', default=False)  # nocomission
    is_active = models.BooleanField('Активен', default=True)

    objects = PromocodeQuerySet.as_manager()

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = 'Промокод'
        verbose_name_plural = 'Промокоды'


class PaymentMethod(models.Model):
    name = models.CharField('Название', max_length=30)
    slug = models.SlugField('Идентификатор', unique=True)
    cardnumber = models.CharField('Номер карты', max_length=30, blank=True, null=True)
    requisites = models.CharField('Реквизиты', max_length=200, blank=True, null=True)

    class Meta:
        verbose_name = 'Метод оплаты'
        verbose_name_plural = 'Методы оплаты'

    def __str__(self):
        return self.name


def image_upload_path(instance, filename):
    dirname = Image.TYPE_TO_UPLOAD_PATH[instance.type]
    return posixpath.join(dirname, filename)


@cleanup.select
class Image(models.Model):
    DEFAULT = 0
    PREVIEW = 1
    DOC = 2
    GIFT = 3

    TYPE_CHOICES = (
        (DEFAULT, 'Изображение'),
        (PREVIEW, 'Превью'),
        (DOC, 'Документ'),
        (GIFT, 'Подарок'),
    )

    TYPE_TO_UPLOAD_PATH = {
        DEFAULT: 'checklist_images/',
        PREVIEW: 'checklist_images/',
        DOC: 'docs/',
        GIFT: 'gifts/',
    }

    image = models.ImageField('Файл изображения', upload_to=image_upload_path)
    type = models.PositiveSmallIntegerField('Тип', choices=TYPE_CHOICES, default=DEFAULT)

    class Meta:
        verbose_name = 'Изображение'
        verbose_name_plural = 'Изображения'

    def __str__(self):
        return f"{self.get_type_display()}: {getattr(self.image, 'name', '')}"


class Gift(models.Model):
    name = models.CharField('Название', max_length=100)
    image = models.ImageField('Фото', upload_to=Image.TYPE_TO_UPLOAD_PATH[Image.GIFT], null=True, blank=True)
    min_price = models.DecimalField('Минимальная цена в юанях', help_text='от какой суммы доступен подарок', max_digits=10, decimal_places=2, default=0)
    available_count = models.PositiveSmallIntegerField('Доступное количество', default=0)

    def __str__(self):
        return self.name

    class Meta:
        verbose_name = 'Подарок'
        verbose_name_plural = 'Подарки'


def generate_checklist_id():
    """ Generate unique id for Checklist """

    all_ids = Checklist.objects.all().values_list('id', flat=True)
    allowed_chars = string.ascii_letters + string.digits

    while True:
        generated_id = ''.join(random.choice(allowed_chars) for _ in range(settings.CHECKLIST_ID_LENGTH))
        if generated_id not in all_ids:
            return generated_id


class ChecklistQuerySet(models.QuerySet):
    def with_base_related(self):
        return self.select_related('manager', 'category', 'category__parent', 'payment_method',
                                   'promocode', 'price_snapshot', 'gift', 'customer') \
            .prefetch_related(Prefetch('images', to_attr='_images'))

    def annotate_bonus_used(self):
        from account.models import BonusProgramTransaction, BonusType

        amount_subquery = Subquery(
            BonusProgramTransaction.objects.all()
            .filter(type=BonusType.SPENT_PURCHASE,
                    user_id=OuterRef('customer_id'),
                    order_id=OuterRef('id'))
            .values('amount'))

        return self.annotate(_bonus_used=Coalesce(amount_subquery, 0))

    def default_ordering(self):
        return self.order_by(F('status_updated_at').desc(nulls_last=True))

    # TODO: deprecate
    def annotate_price_rub(self):
        return self.annotate(
            _yuan_rate=Case(
                When(price_snapshot_id__isnull=False, then=F('price_snapshot__yuan_rate')),
                default=GlobalSettings.load().full_yuan_rate
            ),
            _price_rub=Ceil(F('_yuan_rate') * F('price_yuan'))
        )

    # TODO: deprecate
    def annotate_commission_rub(self):
        default_commission = GlobalSettings.load().commission_rub
        over_150k_commission = F('_price_rub') * settings.COMMISSION_OVER_150K

        category_commission_is_zero_and_parent_present = (
                (Q(category__commission__isnull=True) | Q(category__commission=0)) & Q(category__parent__isnull=False)
        )

        return self.annotate(
            _category_commission_percent=Case(
                When(category_commission_is_zero_and_parent_present, then=F('category__parent__commission')),
                default=F('category__commission')
            ),

            _category_commission=F('_category_commission_percent') * F('_price_rub') / 100,

            _over_150k_commission=Case(
                When(GreaterThan(F("_price_rub"), 150_000), then=over_150k_commission),
                default=0,
                output_field=DecimalField()
            ),

            _commission_rub=Case(
                When(price_snapshot_id__isnull=False, then=F('price_snapshot__commission_rub')),
                default=Max(default_commission, F('_over_150k_commission'), F('_category_commission')),
                output_field=DecimalField()
            ),
        )


class PriceSnapshot(models.Model):
    yuan_rate = models.DecimalField('Курс CNY/RUB', max_digits=10, decimal_places=2, default=0)
    delivery_price_CN = models.DecimalField('Цена доставки по Китаю', max_digits=10, decimal_places=2, default=0)
    delivery_price_CN_RU = models.DecimalField('Цена доставки Китай-РФ', max_digits=10, decimal_places=2, default=0)
    commission_rub = models.DecimalField('Комиссия, руб', max_digits=10, decimal_places=2, default=0)


@cleanup.select
class Checklist(models.Model):
    # Statuses
    class Status:
        DELETED = "deleted"
        DRAFT = "draft"
        NEW = "neworder"
        PAYMENT = "payment"
        BUYING = "buying"
        BOUGHT = "bought"
        CHINA = "china"
        CHINA_RUSSIA = "chinarush"
        RUSSIA = "rush"
        SPLIT_WAITING = "split_waiting"
        SPLIT_PAID = "split_paid"
        CDEK = "cdek"
        COMPLETED = "completed"

        PDF_AVAILABLE_STATUSES = (RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED)
        CDEK_READY_STATUSES = (RUSSIA, SPLIT_PAID, CDEK)

        CANCELLABLE_ORDER_STATUSES = (DRAFT, NEW, PAYMENT, BUYING)
        BONUS_ORDER_STATUSES = (CHINA_RUSSIA, RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED)

        CHOICES = (
            (DELETED, 'Удален'),
            (DRAFT, 'Черновик'),
            (NEW, 'Новый заказ'),
            (PAYMENT, 'Проверка оплаты'),
            (BUYING, 'На закупке'),
            (BOUGHT, 'Закуплен'),
            (CHINA, 'На складе в Китае'),
            (CHINA_RUSSIA, 'Доставка на склад РФ'),
            (RUSSIA, 'На складе в РФ'),
            (SPLIT_WAITING, 'Сплит: ожидание оплаты 2й части'),
            (SPLIT_PAID, 'Сплит: полностью оплачено'),
            (CDEK, 'Доставляется СДЭК'),
            (COMPLETED, 'Завершен'),
        )

    def get_tg_notification(self):
        from tg_bot.messages import TGOrderStatusMessage as msg

        match self.status:
            case Checklist.Status.NEW:
                return msg.NEW.format(order_id=self.id, order_link=self.order_link)

            case Checklist.Status.BUYING:
                if not self.is_split_payment:
                    return msg.BUYING_NON_SPLIT.format(order_id=self.id)
                else:
                    return msg.BUYING_SPLIT.format(order_id=self.id, amount_to_pay=self.split_amount_to_pay())

            case Checklist.Status.BOUGHT:
                return msg.BOUGHT.format(order_id=self.id)

            case Checklist.Status.CHINA:
                return msg.CHINA.format(order_id=self.id)

            case Checklist.Status.CHINA_RUSSIA:
                return msg.CHINA_RUSSIA.format(order_id=self.id)

            case Checklist.Status.RUSSIA:
                if self.delivery == Checklist.DeliveryType.PICKUP:
                    return msg.RUSSIA_PICKUP.format(order_id=self.id)

                elif self.delivery in Checklist.DeliveryType.CDEK_TYPES:
                    return msg.RUSSIA_CDEK.format(order_id=self.id)

            case Checklist.Status.SPLIT_WAITING:
                if self.delivery == Checklist.DeliveryType.PICKUP:
                    return msg.SPLIT_WAITING_PICKUP.format(order_id=self.id, amount_to_pay=self.split_amount_to_pay)

                elif self.delivery in Checklist.DeliveryType.CDEK_TYPES:
                    return msg.SPLIT_WAITING_CDEK.format(order_id=self.id, amount_to_pay=self.split_amount_to_pay)

            # FIXME: split_accepted ?
            case Checklist.Status.SPLIT_PAID:
                return msg.SPLIT_PAID.format(order_id=self.id)

            case Checklist.Status.CDEK:
                return msg.CDEK.format(order_id=self.id)

            case Checklist.Status.COMPLETED:
                return msg.COMPLETED.format(order_id=self.id)

            case _:
                return None

    @property
    def order_link(self):
        return f"https://poizonstore.com/orderpageinprogress/{self.id}"

    def split_amount_to_pay(self):
        # FIXME: it's stupid, create PaymentInfo model or something
        return self.full_price // 2

    # Delivery
    class DeliveryType:
        PICKUP = "pickup"
        CDEK = "cdek"
        CDEK_COURIER = "cdek_courier"

        CHOICES = (
            (PICKUP, 'Самовывоз из шоурума'),
            (CDEK, 'Пункт выдачи заказов CDEK'),
            (CDEK_COURIER, 'Курьерская доставка CDEK'),
        )

        CDEK_TYPES = (CDEK, CDEK_COURIER)

    created_at = models.DateTimeField(auto_now_add=True)
    status_updated_at = models.DateTimeField('Дата обновления статуса заказа', auto_now_add=True, editable=False)
    id = models.CharField(primary_key=True, max_length=settings.CHECKLIST_ID_LENGTH,
                          default=generate_checklist_id, editable=False)
    status = models.CharField('Статус заказа', max_length=15, choices=Status.CHOICES, default=Status.NEW)
    # managerid
    manager = models.ForeignKey('account.User', verbose_name='Менеджер', related_name='manager_orders',
                                on_delete=models.SET_NULL, blank=True, null=True)
    product_link = models.URLField('Ссылка на товар', null=True, blank=True)
    category = models.ForeignKey('Category', verbose_name="Категория", blank=True, null=True, on_delete=models.SET_NULL)

    brand = models.CharField('Бренд', max_length=100, null=True, blank=True)
    model = models.CharField('Модель', max_length=100, null=True, blank=True)
    size = models.CharField('Размер', max_length=30, null=True, blank=True)

    # curencycurency2
    price_yuan = models.DecimalField('Цена в юанях', max_digits=10, decimal_places=2, default=0)
    # TODO: replace real_price by parser
    real_price = models.DecimalField('Реальная цена', max_digits=10, decimal_places=2, null=True, blank=True)

    # promo
    promocode = models.ForeignKey('Promocode', verbose_name='Промокод', on_delete=models.PROTECT, null=True, blank=True)
    gift = models.ForeignKey('Gift', verbose_name='Подарок', on_delete=models.SET_NULL, null=True, blank=True)
    comment = models.CharField('Комментарий', max_length=200, null=True, blank=True)

    customer = models.ForeignKey('account.User', on_delete=models.CASCADE, related_name='customer_orders', blank=True,
                                 null=True)

    # receivername
    receiver_name = models.CharField('Имя получателя', max_length=100, null=True, blank=True)
    # reveiverphone
    receiver_phone = models.CharField('Телефон получателя', max_length=100, null=True, blank=True)

    is_split_payment = models.BooleanField('Оплата частями', default=False)

    # paymenttype
    payment_method = models.ForeignKey('PaymentMethod', verbose_name='Метод оплаты',
                                       null=True, blank=True,
                                       on_delete=models.SET_NULL)

    images = models.ManyToManyField('Image', verbose_name='Изображения', related_name='+', blank=True)
    payment_proof = models.ImageField('Подтверждение оплаты', upload_to=Image.TYPE_TO_UPLOAD_PATH[Image.DOC],
                                      null=True, blank=True)  # paymentprovement
    split_payment_proof = models.ImageField('Подтверждение оплаты сплита',
                                            upload_to=Image.TYPE_TO_UPLOAD_PATH[Image.DOC],
                                            null=True, blank=True)
    split_accepted = models.BooleanField('Сплит принят', default=False)

    receipt = models.ImageField('Фото чека', upload_to=Image.TYPE_TO_UPLOAD_PATH[Image.DOC], null=True,
                                blank=True)  # checkphoto

    delivery = models.CharField('Тип доставки', max_length=15, choices=DeliveryType.CHOICES, null=True, blank=True)
    # trackid
    poizon_tracking = models.CharField('Трек-номер Poizon', max_length=100, null=True, blank=True)
    cdek_tracking = models.CharField('Трек-номер СДЭК', max_length=100, null=True, blank=True)
    cdek_barcode_pdf = models.FileField('Штрих-код СДЭК в PDF', upload_to='docs', null=True, blank=True)

    price_snapshot = models.ForeignKey('PriceSnapshot', verbose_name='Сохраненные цены',
                                       related_name='checklist',
                                       on_delete=models.SET_NULL, null=True, blank=True)

    objects = ChecklistQuerySet.as_manager()

    class Meta:
        verbose_name = 'Заказ'
        verbose_name_plural = 'Заказы'

    @property
    def buy_time_remaining(self) -> Optional[timedelta]:
        if self.status != Checklist.Status.NEW:
            return None

        time_to_buy = GlobalSettings.load().time_to_buy
        diff = max(timedelta(), timezone.now() - self.status_updated_at)
        result = max(timedelta(), time_to_buy - diff)
        return result

    @property
    def price_rub(self) -> int:
        # Get saved prices
        if self.price_snapshot_id:
            yuan_rate = self.price_snapshot.yuan_rate
        else:
            yuan_rate = GlobalSettings.load().full_yuan_rate

        return math.ceil(yuan_rate * self.price_yuan)

    @property
    def full_price(self) -> int:
        price = self.price_rub

        free_delivery = False
        no_comission = False

        if self.promocode_id:
            # We assume that working promocode was bound correctly through serializer
            # and intentionally don't check if promocode is active here.
            # It's also good for archive orders.
            promocode = self.promocode
            price -= min(self.price_rub, promocode.discount)

            free_delivery = promocode.free_delivery
            no_comission = promocode.no_comission

        if not free_delivery:
            price += self.delivery_price_CN + self.delivery_price_CN_RU

        if not no_comission:
            price += self.commission_rub

        return max(0, math.ceil(price))

    @property
    def yuan_rate(self) -> Decimal:
        # Get saved value if exists
        if self.price_snapshot_id:
            return self.price_snapshot.yuan_rate
        else:
            return GlobalSettings.load().full_yuan_rate

    @property
    def delivery_price_CN(self) -> Decimal:
        # Get saved value if exists
        if self.price_snapshot_id:
            return self.price_snapshot.delivery_price_CN
        else:
            return GlobalSettings.load().delivery_price_CN

    @property
    def delivery_price_CN_RU(self) -> Decimal:
        # Get saved value if exists
        if self.price_snapshot_id:
            return self.price_snapshot.delivery_price_CN_RU
        else:
            return getattr(self.category, 'delivery_price', Decimal(0))

    @property
    def commission_rub(self) -> Decimal:
        # Prefer saved value
        if self.price_snapshot_id:
            return self.price_snapshot.commission_rub

        # Default commission
        commission = GlobalSettings.load().commission_rub

        # For big orders there is an additional commission
        if self.price_rub > 150_000:
            commission = max(commission, self.price_rub * Decimal(settings.COMMISSION_OVER_150K))

        if self.category_id:
            # Add commission of bottom-most category
            category_commission = getattr(self.category, 'commission_price', 0)
            commission = max(commission, category_commission * self.price_rub / 100)

        return commission

    @property
    def bonus_used(self):
        # Prefer annotated field
        if hasattr(self, '_bonus_used'):
            return self._bonus_used

        if self.customer_id is None:
            return 0

        # It is guaranteed that there is only one SPENT_PURCHASE for this order
        transaction = self.customer.bonus_history.filter(order_id=self.id).first()
        return abs(transaction.amount) if transaction else 0

    @atomic()
    def cancel(self):
        """ Cancel the order and return all bonuses """

        # Don't delete orders entirely, just change the status
        if self.status == Checklist.Status.DRAFT:
            self.status = Checklist.Status.DELETED
        else:
            self.status = Checklist.Status.DRAFT

        self.save()

        if self.customer_id is None:
            return

        self.customer.bonus_history.filter(order_id=self.id, was_cancelled=False).cancel()

    @property
    def preview_image(self):
        # Prefer annotated field
        if hasattr(self, '_images'):
            # Get first preview image from all images
            return next(filter(lambda x: x.type == Image.PREVIEW, self._images), None)

        return self.images.filter(type=Image.PREVIEW).first()

    @property
    def preview_image_url(self):
        return getattr(self.preview_image, 'image', None)

    @property
    def main_images(self) -> list:
        # Prefer prefetched field
        if hasattr(self, '_images'):
            return [img for img in self._images if img.type == Image.DEFAULT]

        return self.images.filter(type=Image.DEFAULT)

    def __str__(self):
        return f'{self.id}'

    def generate_preview(self, source_img: Image = None):
        source_img = source_img or next(iter(self.main_images), None)
        if not source_img:
            return

        title_lines = [v for v in (self.brand, self.model) if v is not None]
        # Render preview image
        preview = create_preview(source_img.image.path,
                                 size=self.size,
                                 price_rub=self.full_price,
                                 title_lines=title_lines)

        # Prepare bytes
        image_io = BytesIO()
        preview.save(image_io, format='JPEG')

        # Create Image model and save it
        image_obj = Image(type=Image.PREVIEW)
        image_obj.image.save(name=f'{self.id}_preview.jpg',
                             content=ContentFile(image_io.getvalue()),
                             save=True)

        self.images.add(image_obj)

    def save_prices(self):
        # Temporarily remove snapshot from object
        self.price_snapshot = None

        snapshot, _ = PriceSnapshot.objects.get_or_create(
            checklist__id=self.id,
            defaults={
                'yuan_rate': self.yuan_rate,
                'delivery_price_CN': self.delivery_price_CN,
                'delivery_price_CN_RU': self.delivery_price_CN_RU,
                'commission_rub': self.commission_rub,
            }
        )

        # Restore snapshot
        self.price_snapshot = snapshot

    def _notify_about_status_change(self):
        if self.customer_id is None:
            return

        tg_message = self.get_tg_notification()
        if tg_message:
            self.customer.notify_tg_bot(tg_message)

    @atomic
    def _check_eligible_for_order_bonus(self):
        if self.customer_id is None:
            return

        if self.status != Checklist.Status.CHINA_RUSSIA:
            return

        # Check if any BonusProgramTransaction bound to current order exists
        from account.models import BonusProgramTransaction
        if BonusProgramTransaction.objects.filter(order_id=self.id).exists():
            return

        # Apply either referral bonus or order bonus, not both
        if self.customer.inviter is not None and self.customer.customer_orders.count() == 1:
            self.customer.add_referral_bonus(self, for_inviter=False)
            self.customer.inviter.add_referral_bonus(self, for_inviter=True)
        else:
            self.customer.add_order_bonus(self)

    # TODO: split into sub-functions
    def save(self, *args, **kwargs):
        if self.id:
            old_obj = Checklist.objects.filter(id=self.id).first()

            self._check_eligible_for_order_bonus()

            # If status was updated, update status_updated_at field
            if old_obj is not None and self.status != old_obj.status:
                self.status_updated_at = timezone.now()

                self._notify_about_status_change()
                # TODO: remove bonuses if order is canceled?

            # Invalidate old CDEK barcode PDF
            if not self.cdek_barcode_pdf or self.cdek_tracking != old_obj.cdek_tracking:
                self.cdek_barcode_pdf.delete(save=False)
                self.cdek_barcode_pdf = None

            # Try to get CDEK barcode PDF
            if not self.cdek_barcode_pdf and self.cdek_tracking and self.status in Checklist.Status.PDF_AVAILABLE_STATUSES:
                from store.views import CDEKAPI
                pdf_file = CDEKAPI.client.get_barcode_file(self.cdek_tracking)
                if pdf_file:
                    self.cdek_barcode_pdf.save(f'{self.id}_barcode.pdf', pdf_file, save=False)

            # Invalidate old preview_image if full_price changed
            price_changed = old_obj is not None and self.full_price != old_obj.full_price
            if price_changed:
                self.preview_image.delete(save=False)

            # Create preview image
            if self.preview_image is None:
                self.generate_preview()

            # Update available gifts count
            old_gift = getattr(old_obj, 'gift', None)
            if self.gift != old_gift:
                # Decrement new gift
                if self.gift:
                    self.gift.available_count = max(0, self.gift.available_count - 1)
                    self.gift.save()

                # Increment new gift
                if old_gift:
                    old_gift.available_count = max(0, old_gift.available_count + 1)
                    old_gift.save()

        # Save price details to snapshot
        if self.price_snapshot_id:
            # Status updated from other statuses back to DRAFT
            if self.status == Checklist.Status.DRAFT:
                self.price_snapshot.delete()
                self.price_snapshot = None

        elif self.status != Checklist.Status.DRAFT:
            self.save_prices()

        super().save(*args, **kwargs)

