"""
Сигналы для автоматического управления резервами и списаниями.

Подключаются при создании, изменении и удалении заказов.
"""

import threading
from django.db.models.signals import post_save, pre_delete, post_delete, pre_save
from django.db.models import Q
from django.db import transaction
from django.dispatch import receiver
from django.utils import timezone
from decimal import Decimal

from django.core.exceptions import ValidationError

from orders.models import Order, OrderItem
from inventory.models import Reservation, Warehouse, StockBatch, Sale, SaleBatchAllocation, Inventory, WriteOff, Stock, WriteOffDocumentItem, Transformation, TransformationInput, TransformationOutput
from inventory.services import SaleProcessor
from inventory.services.batch_manager import StockBatchManager
# InventoryProcessor больше не используется в сигналах - обработка вызывается явно через view

# ============================================================================
# Thread-local storage для временных флагов управления сигналами
# ============================================================================
_skip_sale_creation = threading.local()


def skip_sale_creation():
    """Установить флаг для пропуска создания Sale в сигнале."""
    _skip_sale_creation.value = True


def reset_sale_creation():
    """Сбросить флаг пропуска создания Sale."""
    _skip_sale_creation.value = False


def is_skip_sale_creation():
    """Проверить, установлен ли флаг пропуска создания Sale."""
    return getattr(_skip_sale_creation, 'value', False)


# ============================================================================
# pre_save сигнал для сохранения предыдущего статуса Order
# ============================================================================

@receiver(pre_save, sender='orders.Order')
def store_previous_order_status(sender, instance, **kwargs):
    """
    Сохраняет предыдущий статус перед сохранением заказа.

    Используется в post_save сигналах для отслеживания изменений статуса
    без использования django-simple-history (который не работает с tenant схемами).
    """
    if instance.pk:
        try:
            instance._previous_status = sender.objects.get(pk=instance.pk).status
        except sender.DoesNotExist:
            instance._previous_status = None
    else:
        instance._previous_status = None


def update_is_returned_flag(order):
    """
    Обновляет флаг is_returned на основе фактического состояния заказа.

    Логика (упрощенная без history):
    - Если есть хотя бы одна Sale по этому заказу → is_returned = False (заказ активен)
    - Если Sale нет → is_returned = True (продажи были, но откачены/удалены)

    Это гарантирует что флаг отражает реальность:
    - Заказ продан и не возвращён → False
    - Заказ был продан, но продажи откачены (возврат) → True
    - Новый заказ без продаж → False (но при первом создании Sale станет False)
    """
    has_sale_now = Sale.objects.filter(order=order).exists()

    if has_sale_now:
        # Есть актуальные продажи → заказ не возвращён
        new_flag = False
    else:
        # Продаж нет → заказ возвращен (если продажи были, они откачены)
        new_flag = True

    # Обновляем только если значение изменилось
    if order.is_returned != new_flag:
        Order.objects.filter(pk=order.pk).update(is_returned=new_flag)


def check_released_reservations_available(order):
    """
    Проверяет, доступны ли товары из освобождённых резервов для повторной продажи.

    Используется при попытке вернуть отменённый заказ в статус выполнения.
    Проверяет что товары из released резервов ещё не использованы в других заказах.

    Args:
        order: Order instance with is_returned=True

    Returns:
        bool: True если все товары доступны, False если хотя бы один использован

    Logic:
        - Для каждого released резерва проверяем Stock.quantity_free
        - quantity_free = quantity_available - quantity_reserved
        - Если quantity_free >= reservation.quantity для ВСЕХ резервов → True
        - Иначе → False (товары частично/полностью использованы в других заказах)
    """
    import logging
    logger = logging.getLogger(__name__)

    from inventory.models import Stock, Reservation

    released_reservations = Reservation.objects.filter(
        order_item__order=order,
        status='released'
    ).select_related('product', 'warehouse')

    if not released_reservations.exists():
        # Нет released резервов - ничего проверять не нужно
        return True

    for reservation in released_reservations:
        # Получаем текущее состояние склада
        stock = Stock.objects.filter(
            product=reservation.product,
            warehouse=reservation.warehouse
        ).first()

        if not stock:
            # Нет записи Stock = товара нет на складе
            logger.warning(
                f"Заказ {order.order_number}: нет Stock для {reservation.product.name} "
                f"на складе {reservation.warehouse.name}"
            )
            return False

        # Обновляем Stock на случай рассинхронизации
        stock.refresh_from_batches()

        # Проверяем свободное количество (доступное минус зарезервированное)
        if stock.quantity_free < reservation.quantity:
            logger.info(
                f"Заказ {order.order_number}: недостаточно свободного товара "
                f"{reservation.product.name}. Нужно: {reservation.quantity}, "
                f"доступно свободно: {stock.quantity_free} "
                f"(всего: {stock.quantity_available}, зарезервировано: {stock.quantity_reserved})"
            )
            return False

    # Все товары доступны
    logger.info(
        f"✅ Заказ {order.order_number}: все товары из released резервов доступны "
        f"для повторной продажи ({released_reservations.count()} позиций)"
    )
    return True


@receiver(post_save, sender='orders.OrderItem')
def reserve_stock_on_item_create(sender, instance, created, **kwargs):
    """
    Сигнал: При создании позиции заказа резервируем товар.
    
    ВАЖНО: Срабатывает ПОСЛЕ OrderItem.save(), когда quantity_in_base_units уже вычислено!
    
    Процесс:
    1. Проверяем, новая ли позиция (создана только что)
    2. Для обычных товаров - создаём резерв с учетом единиц продажи
    3. Для комплектов - резервируем компоненты (группируя одинаковые товары)
    4. Для ВИТРИННЫХ комплектов - НЕ создаём резервы (они уже есть от ShowcaseItem)
    5. Статус резерва = 'reserved'
    6. Проверяем на существующие резервы (защита от дубликатов)
    """
    from collections import defaultdict
    
    if not created:
        return  # Только для новых позиций
    
    order = instance.order
    
    # Определяем склад (используем склад самовывоза из заказа или первый активный)
    warehouse = order.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()
    
    if not warehouse:
        # Если нет активных складов, зарезервировать не можем
        return
    
    # Резервируем товар или комплект
    if instance.product:
        # Обычный товар - резервируем с учетом единиц продажи
        # quantity_in_base_units УЖЕ вычислено в OrderItem.save()
        reservation_quantity = instance.quantity_in_base_units if instance.quantity_in_base_units else Decimal(str(instance.quantity))
        _create_or_update_reservation(
            instance,
            instance.product,
            warehouse,
            reservation_quantity,
            sales_unit=instance.sales_unit
        )
    
    elif instance.product_kit and instance.kit_snapshot:
        # КРИТИЧНО: Проверяем витринный ли это комплект
        is_showcase_kit = instance.product_kit.is_temporary and instance.product_kit.showcase
        
        if is_showcase_kit:
            # Витринный комплект - резервы УЖЕ созданы через ShowcaseManager.reserve_kit_to_showcase
            # Привязка резервов к OrderItem происходит в update_reservation_on_item_change
            # НЕ создаём новые резервы!
            import logging
            logger = logging.getLogger(__name__)
            logger.info(
                f"ℹ️ Витринный комплект '{instance.product_kit.name}': пропускаем создание резервов "
                f"(уже созданы ShowcaseManager), OrderItem #{instance.id}"
            )
            return
        
        # Обычный (постоянный) комплект - резервируем КОМПОНЕНТЫ из снимка
        # Группируем одинаковые товары для создания одного резерва
        product_quantities = defaultdict(Decimal)
        
        for kit_item in instance.kit_snapshot.items.select_related('original_product'):
            if kit_item.original_product:
                # Рассчитываем количество одного компонента в базовых единицах
                component_qty_base = kit_item.quantity
                if kit_item.conversion_factor and kit_item.conversion_factor > 0:
                    component_qty_base = kit_item.quantity / kit_item.conversion_factor

                # Суммируем количество: qty компонента (base) * qty комплектов в заказе
                product_quantities[kit_item.original_product_id] += (
                    component_qty_base * Decimal(str(instance.quantity))
                )
        
        # Создаём по одному резерву на каждый уникальный товар
        from products.models import Product
        for product_id, total_qty in product_quantities.items():
            product = Product.objects.get(pk=product_id)
            _create_or_update_reservation(
                instance, product, warehouse, total_qty, product_kit=instance.product_kit
            )


def _create_or_update_reservation(order_item, product, warehouse, quantity, product_kit=None, sales_unit=None):
    """
    Вспомогательная функция для создания или обновления резерва.
    
    Args:
        order_item: Позиция заказа
        product: Товар
        warehouse: Склад
        quantity: Количество (в базовых единицах)
        product_kit: Комплект (для резервов компонентов)
        sales_unit: Единица продажи (опционально)
    """
    # Формируем фильтр для поиска существующего резерва
    filter_kwargs = {
        'order_item': order_item,
        'product': product,
        'warehouse': warehouse,
    }
    if product_kit:
        filter_kwargs['product_kit'] = product_kit

    existing_reservation = Reservation.objects.filter(**filter_kwargs).first()

    if existing_reservation:
        # Резерв уже существует - обновляем его
        existing_reservation.quantity = quantity
        existing_reservation.quantity_base = quantity  # quantity уже в базовых единицах
        existing_reservation.sales_unit = sales_unit
        existing_reservation.status = 'reserved'
        existing_reservation.save()
    else:
        # Резерва нет - создаём новый
        Reservation.objects.create(
            order_item=order_item,
            product=product,
            product_kit=product_kit,
            warehouse=warehouse,
            quantity=quantity,
            quantity_base=quantity,  # quantity уже в базовых единицах
            sales_unit=sales_unit,
            status='reserved'
        )


@receiver(post_save, sender=Order)
@transaction.atomic
def create_sale_on_order_completion(sender, instance, created, **kwargs):
    """
    Сигнал: Когда заказ переходит в статус 'completed' (доставлен),
    создается операция Sale и резервы преобразуются в продажу.

    КРИТИЧНО: Резервы обновляются ТОЛЬКО ПОСЛЕ успешного создания Sale!

    ВАЛИДАЦИЯ:
    - Запрещаем переход в положительный финальный статус для заказов с is_returned=True,
      у которых нет резервов (товар уже продан в другом заказе).

    Процесс:
    1. Проверяем, изменился ли статус на 'completed'
    2. ВАЛИДАЦИЯ: если is_returned=True и резервов нет → запрещаем
    3. Для каждого товара создаем Sale (автоматический FIFO-список)
    4. ТОЛЬКО после успешного создания Sale обновляем резервы на 'converted_to_sale'
    5. Обновляем флаг is_returned

    ПРИМЕЧАНИЕ: Если у Order установлен атрибут skip_sale_creation=True,
    создание Sale пропускается (используется в POS для создания Sale после применения скидок).
    """
    import logging
    logger = logging.getLogger(__name__)

    # === ПРОВЕРКА: Пропуск создания Sale по флагу ===
    # Используется в POS checkout, где Sale создаётся явно после применения скидок
    if is_skip_sale_creation():
        logger.info(
            f"ℹ️ Заказ {instance.order_number}: skip_sale_creation=True (thread-local), "
            f"пропускаем автоматическое создание Sale"
        )
        return

    if created:
        return  # Только для обновлений

    # Проверяем наличие статуса (может быть None при создании)
    if not instance.status:
        return
    
    # === ВАЛИДАЦИЯ: Запрет изменения статуса для возвращённых заказов без резервов ===
    # Если заказ был возвращён (is_returned=True) и резервов нет, проверяем доступность товаров
    if instance.is_returned:
        # Исключаем released резервы - проверяем активные резервы
        has_active_reservations = Reservation.objects.filter(
            order_item__order=instance
        ).exclude(status='released').exists()

        if not has_active_reservations:
            # Активных резервов нет — проверяем доступность товаров
            items_available = check_released_reservations_available(instance)

            if not items_available:
                # Товары использованы — блокируем
                if not instance.status.is_negative_end:
                    logger.error(
                        f"❌ Заказ {instance.order_number}: is_returned=True, товары использованы. "
                        f"Попытка установить '{instance.status.name}' запрещена."
                    )
                    raise ValidationError(
                        f"Заказ {instance.order_number} был отменён, товары проданы в другом заказе. "
                        f"Невозможно изменить статус. Для новой продажи создайте новый заказ."
                    )
            else:
                # Товары доступны — разрешаем переход
                logger.info(
                    f"✅ Заказ {instance.order_number}: is_returned=True, но товары доступны. "
                    f"Разрешаем переход в '{instance.status.name}'."
                )
    
    # Проверяем: это положительный финальный статус?
    is_positive_end = instance.status.is_positive_end

    if not is_positive_end:
        return  # Только для положительных финальных статусов (completed и т.п.)

    # === ЗАЩИТА ОТ ПРЕЖДЕВРЕМЕННОГО СОЗДАНИЯ SALE ===
    # Проверяем, есть ли уже Sale для этого заказа
    if Sale.objects.filter(order=instance).exists():
        logger.info(f"Заказ {instance.order_number}: Sale уже существуют, пропускаем")
        update_is_returned_flag(instance)
        return

    # === ЗАЩИТА ОТ RACE CONDITION: Проверяем предыдущий статус ===
    # Если уже были в completed и снова переходим в completed (например completed → draft → completed),
    # проверяем наличие Sale чтобы избежать дублирования
    previous_status = getattr(instance, '_previous_status', None)
    if previous_status and previous_status.is_positive_end:
        logger.info(
            f"Заказ {instance.order_number}: повторный переход в положительный статус "
            f"({previous_status.name} → {instance.status.name}). Проверяем Sale..."
        )
        if Sale.objects.filter(order=instance).exists():
            logger.info(
                f"✓ Заказ {instance.order_number}: Sale уже существуют, пропускаем создание"
            )
            update_is_returned_flag(instance)
            return

    # Проверяем наличие резервов для этого заказа
    # Ищем резервы в статусах 'reserved' (новые) и 'released' (после отката)
    # Исключаем уже обработанные 'converted_to_sale'
    reservations_to_update = Reservation.objects.filter(
        order_item__order=instance
    ).exclude(status='converted_to_sale')
    
    if not reservations_to_update.exists():
        # Проверяем, есть ли вообще резервы (включая released)
        has_any_reservations = Reservation.objects.filter(
            order_item__order=instance
        ).exists()
        
        if has_any_reservations:
            logger.warning(
                f"⚠ Заказ {instance.order_number}: все резервы в статусе 'converted_to_sale' "
                f"или 'released'. Sale уже созданы или резервы освобождены."
            )
        else:
            logger.error(
                f"❌ Заказ {instance.order_number}: нет резервов для создания Sale. "
                f"Заказ в статусе '{instance.status.name}' без возможности списания."
            )
        
        # Обновляем флаг is_returned и выходим
        update_is_returned_flag(instance)
        return

    # Определяем склад (используем склад самовывоза из заказа или первый активный)
    warehouse = instance.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()

    if not warehouse:
        logger.error(f"❌ Не найден склад для заказа {instance.order_number}. Списание невозможно.")
        return

    # Для каждого товара в заказе создаем Sale
    sales_created = []
    sale_creation_failed = False
    
    for item in instance.items.all():
        # Для комплектов (витринных и обычных) создаем Sale для каждого компонента
        if item.product_kit:
            kit = item.product_kit
            
            # Находим все резервы компонентов этого комплекта для этого заказа
            kit_reservations = Reservation.objects.filter(
                order_item=item,
                product_kit=kit
            ).exclude(status='converted_to_sale')
            
            if not kit_reservations.exists():
                # Проверяем, может быть витринный комплект уже продан через ShowcaseManager?
                already_sold = Reservation.objects.filter(
                    order_item=item,
                    product_kit=kit,
                    status='converted_to_sale'
                ).exists()
                
                if already_sold:
                    logger.info(
                        f"ℹ️ Витринный комплект '{kit.name}': резервы уже обработаны через ShowcaseManager.sell_showcase_items"
                    )
                else:
                    logger.warning(
                        f"⚠ Комплект '{kit.name}': не найдено резервов компонентов"
                    )
                continue
            
            # === РАСЧЕТ ЦЕНЫ ===
            # Рассчитываем фактическую стоимость продажи всего комплекта с учетом скидок
            # 1. Базовая стоимость позиции
            item_subtotal = Decimal(str(item.price)) * Decimal(str(item.quantity))

            # 2. Скидки
            item_discount = Decimal(str(item.discount_amount)) if item.discount_amount is not None else Decimal('0')
            
            # Скидка на заказ (распределенная)
            instance.refresh_from_db()
            order_total = instance.subtotal if hasattr(instance, 'subtotal') else Decimal('0')
            delivery_cost = Decimal(str(instance.delivery.cost)) if hasattr(instance, 'delivery') and instance.delivery else Decimal('0')
            order_discount_amount = (order_total - (Decimal(str(instance.total_amount)) - delivery_cost)) if order_total > 0 else Decimal('0')

            if order_total > 0 and order_discount_amount > 0:
                item_order_discount = order_discount_amount * (item_subtotal / order_total)
            else:
                item_order_discount = Decimal('0')

            kit_net_total = item_subtotal - item_discount - item_order_discount
            if kit_net_total < 0:
                kit_net_total = Decimal('0')

            # 3. Суммарная каталожная стоимость всех компонентов (для пропорции)
            total_catalog_price = Decimal('0')
            for reservation in kit_reservations:
                qty = reservation.quantity_base or reservation.quantity
                price = reservation.product.actual_price or Decimal('0')
                total_catalog_price += price * qty
            
            # 4. Коэффициент распределения
            if total_catalog_price > 0:
                ratio = kit_net_total / total_catalog_price
            else:
                # Если каталожная цена 0, распределяем просто по количеству или 0
                ratio = Decimal('0')

            # Создаем Sale для каждого компонента комплекта
            for reservation in kit_reservations:
                try:
                    # Рассчитываем цену продажи компонента пропорционально
                    catalog_price = reservation.product.actual_price or Decimal('0')
                    
                    if ratio > 0:
                        # Распределяем реальную выручку
                        component_sale_price = catalog_price * ratio
                    else:
                        # Если выручка 0 или каталожные цены 0
                        if total_catalog_price == 0 and kit_net_total > 0:
                             # Крайний случай: товаров на 0 руб, а продали за деньги (услуга?)
                             # Распределяем равномерно
                             count = kit_reservations.count()
                             component_qty = reservation.quantity_base or reservation.quantity
                             if count > 0 and component_qty > 0:
                                component_sale_price = (kit_net_total / count) / component_qty
                             else:
                                component_sale_price = Decimal('0')
                        else:
                             component_sale_price = Decimal('0')

                    sale = SaleProcessor.create_sale(
                        product=reservation.product,
                        warehouse=warehouse,
                        quantity=reservation.quantity_base or reservation.quantity,  # Используем quantity_base!
                        sale_price=component_sale_price,
                        order=instance,
                        document_number=instance.order_number
                    )
                    sales_created.append(sale)
                    logger.info(
                        f"✓ Sale создан для компонента комплекта '{kit.name}': "
                        f"{reservation.product.name} - {reservation.quantity_base or reservation.quantity} шт. "
                        f"(цена: {component_sale_price})"
                    )
                except ValueError as e:
                    logger.error(
                        f"❌ ОШИБКА при создании Sale для компонента комплекта '{kit.name}': {e}"
                    )
                    sale_creation_failed = True
                    break
                except Exception as e:
                    logger.error(
                        f"❌ КРИТИЧЕСКАЯ ОШИБКА при создании Sale для комплекта: {e}"
                    )
                    sale_creation_failed = True
                    break
            
            if sale_creation_failed:
                break
            
            continue
        
        # Обычный товар
        product = item.product
        if not product:
            continue

        try:
            # Находим резерв для этого OrderItem
            item_reservation = Reservation.objects.filter(
                order_item=item,
                product=product
            ).exclude(status='converted_to_sale').first()

            if item_reservation:
                # Используем quantity_base из резерва (всегда в базовых единицах)
                sale_quantity = item_reservation.quantity_base or item_reservation.quantity
            else:
                # Fallback: используем quantity_in_base_units из OrderItem
                sale_quantity = item.quantity_in_base_units if item.quantity_in_base_units else Decimal(str(item.quantity))
                logger.warning(
                    f"⚠ Не найден резерв для OrderItem {item.id}. "
                    f"Используем quantity_in_base_units: {sale_quantity}"
                )

            # Цена за единицу с учётом всех скидок (позиция + заказ)
            item_subtotal = Decimal(str(item.price)) * Decimal(str(item.quantity))

            # Скидка на позицию
            item_discount = Decimal(str(item.discount_amount)) if item.discount_amount is not None else Decimal('0')

            # Скидка на заказ (распределяется пропорционально доле позиции в заказе)
            # ВАЖНО: Обновляем Order из БД, чтобы получить актуальный total_amount после применения скидок
            instance.refresh_from_db()
            order_total = instance.subtotal if hasattr(instance, 'subtotal') else Decimal('0')
            # Скидка = subtotal - (total_amount - delivery) (вычитаем доставку, если есть)
            delivery_cost = Decimal(str(instance.delivery.cost)) if hasattr(instance, 'delivery') and instance.delivery else Decimal('0')
            order_discount_amount = (order_total - (Decimal(str(instance.total_amount)) - delivery_cost)) if order_total > 0 else Decimal('0')

            if order_total > 0 and order_discount_amount > 0:
                # Пропорциональная часть скидки заказа для этой позиции
                item_order_discount = order_discount_amount * (item_subtotal / order_total)
            else:
                item_order_discount = Decimal('0')

            total_discount = item_discount + item_order_discount

            if total_discount and item.quantity > 0:
                price_with_discount = (item_subtotal - total_discount) / Decimal(str(item.quantity))
            else:
                price_with_discount = Decimal(str(item.price))

            # Пересчитываем цену в базовые единицы
            if item.sales_unit and item.conversion_factor_snapshot:
                base_price = price_with_discount * item.conversion_factor_snapshot
            else:
                base_price = price_with_discount

            # LOGGING DEBUG INFO
            # print(f"DEBUG SALE PRICE CALCULATION for Item #{item.id} ({product.name}):")
            # print(f"  Price: {item.price}, Qty: {item.quantity}, Subtotal: {item_subtotal}")
            # print(f"  Item Discount: {item_discount}, Order Discount Share: {item_order_discount}, Total Discount: {total_discount}")
            # print(f"  Price w/ Discount: {price_with_discount}")
            # print(f"  Sales Unit: {item.sales_unit}, Conversion: {item.conversion_factor_snapshot}")
            # print(f"  FINAL BASE PRICE: {base_price}")
            # print(f"  Sales Unit Object: {item.sales_unit}")
            # if item.sales_unit:
            #     print(f"    Sales Unit Conversion: {item.sales_unit.conversion_factor}")
            
            logger.info(f"DEBUG SALE PRICE CALCULATION for Item #{item.id} ({product.name}):")
            logger.info(f"  Price: {item.price}, Qty: {item.quantity}, Subtotal: {item_subtotal}")
            logger.info(f"  FINAL BASE PRICE: {base_price}")

            # Создаем Sale (с автоматическим FIFO-списанием)
            sale = SaleProcessor.create_sale(
                product=product,
                warehouse=warehouse,
                quantity=sale_quantity,
                sale_price=base_price,
                order=instance,
                document_number=instance.order_number,
                sales_unit=item.sales_unit  # Передаем sales_unit в Sale
            )
            sales_created.append(sale)
            logger.info(f"✓ Sale создан для {product.name}: {sale_quantity} шт. (базовых единиц)")

        except ValueError as e:
            # Логируем ошибку и прерываем процесс
            logger.error(
                f"❌ ОШИБКА при создании Sale для заказа {instance.order_number}, товар {product.name}: {e}"
            )
            sale_creation_failed = True
            break
        except Exception as e:
            logger.error(
                f"❌ КРИТИЧЕСКАЯ ОШИБКА при создании Sale для заказа {instance.order_number}: {e}"
            )
            sale_creation_failed = True
            break
    
    # КРИТИЧНО: Обновляем резервы ТОЛЬКО если ВСЕ Sale созданы успешно
    if sale_creation_failed:
        logger.error(
            f"❌ Не удалось создать Sale для заказа {instance.order_number}. "
            f"Резервы НЕ будут обновлены. Транзакция откатится."
        )
        # Поднимаем исключение чтобы откатить всю транзакцию
        raise ValueError(f"Не удалось создать Sale для заказа {instance.order_number}")
    
    # Все Sale созданы успешно - теперь обновляем резервы
    if reservations_to_update.exists():
        # Обновляем резервы через .save() чтобы сработал сигнал обновления Stock
        # Сигнал update_stock_on_reservation_change автоматически обновит Stock
        for reservation in reservations_to_update:
            reservation.status = 'converted_to_sale'
            reservation.converted_at = timezone.now()
            reservation.save(update_fields=['status', 'converted_at'])
        
        updated_count = reservations_to_update.count()
        logger.info(
            f"✓ Обновлено {updated_count} резервов для заказа {instance.order_number}: reserved → converted_to_sale"
        )
    
    # === Финализация витринных экземпляров: reserved → sold ===
    # Находим все витринные комплекты в этом заказе в статусе 'reserved'.
    # Прямой переход cancelled → completed НЕВОЗМОЖЕН (автоматически разбивается на draft).
    # Поэтому букеты всегда в 'reserved' при достижении completed.
    from inventory.models import ShowcaseItem
    
    # Ищем по sold_order_item (для букетов в reserved)
    showcase_items_to_finalize = ShowcaseItem.objects.filter(
        sold_order_item__order=instance,
        status='reserved'
    )
    
    finalized_count = 0
    for showcase_item in showcase_items_to_finalize:
        try:
            # Обычный flow: reserved → sold
            showcase_item.mark_sold_from_reserved()
            finalized_count += 1
            logger.info(
                f"✓ Витринный экземпляр #{showcase_item.id} финализирован: reserved → sold"
            )
        except Exception as e:
            logger.error(
                f"❌ Ошибка финализации ShowcaseItem #{showcase_item.id}: {e}",
                exc_info=True
            )
    
    if finalized_count > 0:
        logger.info(
            f"🎉 Финализировано {finalized_count} витринных экземпляров для заказа {instance.order_number}"
        )
    
    logger.info(
        f"🎉 Заказ {instance.order_number} успешно обработан: создано {len(sales_created)} Sale, "
        f"обновлено {reservations_to_update.count() if reservations_to_update.exists() else 0} резервов"
    )
    
    # Обновляем флаг is_returned на основе фактического состояния
    update_is_returned_flag(instance)


@receiver(post_save, sender=Order)
@transaction.atomic
def rollback_sale_on_status_change(sender, instance, created, **kwargs):
    """
    Сигнал: Откатить Sale при уходе от статуса 'completed'.

    Триггер: completed → любой другой статус (draft, cancelled и т.д.)

    Процесс:
    1. Отслеживаем переход ОТ статуса 'completed'
    2. Удаляем Sale и восстанавливаем StockBatch через SaleBatchAllocation
    3. Обновляем резервы:
       - Обычные резервы: reserved или released в зависимости от сценария
       - Витринные временные комплекты: ВСЕГДА reserved (остаются на витрине)
    4. Обновляем Stock

    Сценарии:
    - А (ошибка/возврат): completed → draft/in_delivery → обычные резервы в 'reserved'
    - Б (отмена): completed → cancelled → обычные резервы в 'released'
    - В (витринные комплекты): любой уход от completed → резервы в 'reserved' (букет остаётся на витрине)
    
    ПРИМЕЧАНИЕ: Этот сигнал ОБРАБАТЫВАЕТ ТОЛЬКО переход ОТ 'completed'!
    Для перехода к 'cancelled' из любого статуса см. release_reservations_on_cancellation
    """
    import logging
    logger = logging.getLogger(__name__)

    # Пропускаем новые заказы
    if created:
        return

    # Проверяем наличие статуса
    if not instance.status:
        return

    current_status = instance.status

    # === Получаем предыдущий статус из pre_save сигнала ===
    previous_status = getattr(instance, '_previous_status', None)
    if not previous_status:
        return  # Нет предыдущего статуса (новый заказ или первое сохранение)

    # === Проверяем: был ли переход ОТ 'completed'? ===
    if previous_status.code != 'completed':
        return  # Не было перехода от 'completed', выходим

    # Проверяем: не остались ли мы в 'completed'?
    if current_status.code == 'completed':
        return  # Всё ещё 'completed', откат не нужен

    # === Если дошли сюда: был переход completed → другой_статус ===
    logger.info(
        f"🔄 Обнаружен уход от статуса 'completed' для заказа {instance.order_number}: "
        f"{previous_status.name} → {current_status.name}"
    )

    # === Определяем сценарий отката ===
    is_cancellation = current_status.is_negative_end  # True для 'cancelled'

    logger.info(
        f"  Проверка сценария: current_status.code='{current_status.code}', "
        f"is_negative_end={current_status.is_negative_end}, is_cancellation={is_cancellation}"
    )

    if is_cancellation:
        # Сценарий Б: отмена заказа
        reservation_target_status = 'released'
        logger.info(f"  Сценарий Б: Отмена заказа (статус: {current_status.name})")
    else:
        # Сценарий А: возврат к промежуточному статусу
        reservation_target_status = 'reserved'
        logger.info(f"  Сценарий А: Возврат к промежуточному статусу (статус: {current_status.name})")

    # === Откатываем Sale ===
    sales = Sale.objects.filter(order=instance).select_related('product', 'warehouse')

    if not sales.exists():
        logger.warning(
            f"⚠ Для заказа {instance.order_number} нет Sale для отката. "
            f"Обновляем только резервы."
        )
    else:
        # Группируем Stock для обновления
        stocks_to_refresh = set()
        sales_count = sales.count()
        restored_count = 0

        try:
            for sale in sales:
                # Запоминаем product и warehouse для обновления Stock
                # (важно для pending продаж, которые учитываются в Stock.refresh_from_batches())
                stocks_to_refresh.add((sale.product, sale.warehouse))

                # Находим все распределения партий
                allocations = SaleBatchAllocation.objects.filter(sale=sale).select_related('batch')

                if not allocations.exists():
                    # Для pending продаж (is_pending_cost=True) это нормально -
                    # партии ещё не были созданы. При удалении Sale
                    # Stock.refresh_from_batches() автоматически уберёт pending из расчёта.
                    if sale.is_pending_cost:
                        logger.info(
                            f"  Sale {sale.id} - pending продажа (в минус). "
                            f"Удаляем Sale, Stock обновится автоматически."
                        )
                    else:
                        logger.warning(
                            f"⚠ Sale {sale.id} не имеет SaleBatchAllocation. "
                            f"Удаляем Sale без восстановления товара."
                        )
                    sale.delete()
                    continue

                # Восстанавливаем товар в каждой партии
                for alloc in allocations:
                    try:
                        batch = alloc.batch

                        logger.debug(
                            f"  Восстанавливаем партию {batch.id}: "
                            f"+{alloc.quantity} для {sale.product.name}"
                        )

                        batch.quantity += alloc.quantity
                        batch.is_active = True  # Реактивируем если была деактивирована
                        batch.save(update_fields=['quantity', 'is_active', 'updated_at'])

                        restored_count += 1

                        # Запоминаем что нужно обновить Stock
                        stocks_to_refresh.add((sale.product, sale.warehouse))

                    except Exception as e:
                        logger.error(
                            f"❌ Ошибка при восстановлении партии {alloc.batch_id} "
                            f"для Sale {sale.id}: {e}",
                            exc_info=True
                        )
                        # Не прерываем процесс, продолжаем с другими партиями

                # Удаляем Sale (каскадно удалит SaleBatchAllocation)
                sale.delete()

            logger.info(
                f"✓ Удалено {sales_count} Sale, "
                f"восстановлено {restored_count} партий товара"
            )

            # Обновляем все затронутые Stock записи
            for product, warehouse in stocks_to_refresh:
                try:
                    stock, _ = Stock.objects.get_or_create(
                        product=product,
                        warehouse=warehouse
                    )
                    stock.refresh_from_batches()

                    logger.debug(
                        f"  Stock обновлен для {product.name} на {warehouse.name}: "
                        f"доступно={stock.quantity_available}, "
                        f"зарезервировано={stock.quantity_reserved}"
                    )

                except Exception as e:
                    logger.error(
                        f"❌ Ошибка при обновлении Stock для {product.name}: {e}",
                        exc_info=True
                    )

        except Exception as e:
            logger.error(
                f"❌ Критическая ошибка при откате Sale для заказа {instance.order_number}: {e}",
                exc_info=True
            )
            # transaction.atomic откатит все изменения
            raise

    # === Обновляем резервы ===
    reservations = Reservation.objects.filter(
        order_item__order=instance,
        status='converted_to_sale'
    )

    # Разделяем резервы на витринные временные комплекты и обычные
    # Витринные временные комплекты: is_temporary=True и showcase не null
    showcase_kit_reservations = reservations.filter(
        product_kit__is_temporary=True,
        product_kit__showcase__isnull=False
    )
    
    # Обычные резервы (все остальные)
    normal_reservations = reservations.exclude(
        id__in=showcase_kit_reservations.values_list('id', flat=True)
    )
    
    showcase_count = showcase_kit_reservations.count()
    normal_count = normal_reservations.count()
    total_count = showcase_count + normal_count

    if total_count > 0:
        # Обновляем обычные резервы согласно сценарию (released при отмене, reserved при возврате)
        if normal_count > 0:
            for reservation in normal_reservations:
                reservation.status = reservation_target_status
                if reservation_target_status == 'released':
                    reservation.released_at = timezone.now()
                # converted_at оставляем (для истории)
                
                # Используем save() с указанием измененных полей
                update_fields = ['status']
                if reservation_target_status == 'released':
                    update_fields.append('released_at')
                reservation.save(update_fields=update_fields)
            
            logger.info(
                f"✓ Обновлено {normal_count} обычных резервов: "
                f"converted_to_sale → {reservation_target_status}"
            )
        
        # Витринные временные комплекты: логика зависит от сценария
        if showcase_count > 0:
            if is_cancellation:
                # Сценарий Б: Отмена - возвращаем на витрину
                for reservation in showcase_kit_reservations:
                    reservation.status = 'reserved'
                    # КРИТИЧНО: Отвязываем резервы от заказа при отмене
                    # Сохраняем ID для защиты от кражи резервов другими заказами
                    if reservation.order_item:
                        reservation.original_order_item_id = reservation.order_item.id
                    reservation.order_item = None
                    # Очищаем блокировки корзины
                    reservation.cart_lock_expires_at = None
                    reservation.locked_by_user = None
                    reservation.cart_session_id = None
                    # showcase_item и product_kit остаются - букет на витрине
                    # converted_at оставляем (для истории)
                    reservation.save(update_fields=['status', 'order_item', 'original_order_item_id',
                                                    'cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])
                
                logger.info(
                    f"✓ Обновлено {showcase_count} резервов витринных комплектов: "
                    f"converted_to_sale → reserved (возвращены на витрину, отвязаны от заказа, блокировки сняты)"
                )
            else:
                # Сценарий А: Возврат к нейтральному - резервы ОСТАЮТСЯ в заказе
                for reservation in showcase_kit_reservations:
                    reservation.status = 'reserved'
                    # Очищаем ТОЛЬКО блокировки корзины
                    # order_item НЕ ТРОГАЕМ - резерв остаётся за заказом!
                    reservation.cart_lock_expires_at = None
                    reservation.locked_by_user = None
                    reservation.cart_session_id = None
                    # converted_at оставляем (для истории)
                    reservation.save(update_fields=['status', 'cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])
                
                logger.info(
                    f"✓ Обновлено {showcase_count} резервов витринных комплектов: "
                    f"converted_to_sale → reserved (остаются в заказе, блокировки сняты)"
                )
    else:
        logger.warning(
            f"⚠ Для заказа {instance.order_number} нет резервов в статусе 'converted_to_sale'"
        )

    # === Возвращаем витринные экземпляры обратно на витрину ===
    # ТОЛЬКО при отмене (отрицательный статус)!
    logger.info(
        f"  Проверка возврата ShowcaseItem: is_cancellation={is_cancellation}"
    )
    
    if is_cancellation:
        from inventory.models import ShowcaseItem
            
        # Находим все ShowcaseItem, проданные в рамках этого заказа
        showcase_items = ShowcaseItem.objects.filter(
            sold_order_item__order=instance,
            status='sold'
        )
            
        showcase_items_count = showcase_items.count()
        
        logger.info(
            f"  Найдено {showcase_items_count} ShowcaseItem для возврата на витрину"
        )
            
        if showcase_items_count > 0:
            logger.info(
                f"🔄 Возвращаем {showcase_items_count} витринных экземпляров обратно на витрину..."
            )
                
            # Возвращаем каждый экземпляр на витрину
            for item in showcase_items:
                item.status = 'available'
                item.sold_order_item = None
                item.sold_at = None
                # showcase и product_kit не трогаем - букет остаётся на той же витрине
                item.save(update_fields=['status', 'sold_order_item', 'sold_at', 'updated_at'])
                    
                # КРИТИЧНО: Восстанавливаем связь между ShowcaseItem и Reservation
                # Находим все резервы этого комплекта для данного OrderItem
                order_item = item.sold_order_item if hasattr(item, '_original_sold_order_item') else None
                if not order_item:
                    # Пытаемся найти через заказ и product_kit
                    order_items = instance.items.filter(product_kit=item.product_kit)
                    if order_items.exists():
                        order_item = order_items.first()
                    
                if order_item:
                    # Восстанавливаем связь showcase_item в резервах через .save()
                    reservations = Reservation.objects.filter(
                        order_item=order_item,
                        product_kit=item.product_kit,
                        status='reserved'
                    )

                    reservations_updated = 0
                    for reservation in reservations:
                        reservation.showcase_item = item
                        reservation.save(update_fields=['showcase_item'])
                        reservations_updated += 1

                    if reservations_updated > 0:
                        logger.debug(
                            f"  ✅ Восстановлена связь для {reservations_updated} резервов ShowcaseItem #{item.id}"
                        )
                
            logger.info(
                f"✅ {showcase_items_count} витринных экземпляров вернулись на витрину: sold → available со связью с резервами"
            )
    else:
        # Сценарий А: Возврат к нейтральному - ShowcaseItem sold → reserved
        from inventory.models import ShowcaseItem
        
        # Находим все ShowcaseItem в статусе 'sold' для этого заказа
        showcase_items_to_unreserve = ShowcaseItem.objects.filter(
            sold_order_item__order=instance,
            status='sold'
        )
        
        unreserved_count = 0
        for showcase_item in showcase_items_to_unreserve:
            try:
                # Возвращаем в reserved (букет остаётся занят под заказ)
                showcase_item.return_to_reserved(showcase_item.sold_order_item)
                unreserved_count += 1
                logger.info(
                    f"✓ Витринный экземпляр #{showcase_item.id} возвращён в резерв: sold → reserved"
                )
            except Exception as e:
                logger.error(
                    f"❌ Ошибка возврата ShowcaseItem #{showcase_item.id} в reserved: {e}"
                )
        
        if unreserved_count > 0:
            logger.info(
                f"🔄 {unreserved_count} витринных экземпляров возвращено в резерв: sold → reserved (заказ в нейтральном статусе)"
            )
        else:
            logger.info(
                f"ℹ️ Сценарий А: Нет витринных экземпляров для возврата в reserved"
            )
    
    # === Обновляем is_returned ===
    # Используем единую функцию для обновления флага на основе фактического состояния
    update_is_returned_flag(instance)

    logger.info(
        f"🎉 Откат для заказа {instance.order_number} завершён успешно: "
        f"{previous_status.name} → {current_status.name}"
    )


@receiver(post_save, sender=Order)
@transaction.atomic
def release_reservations_on_cancellation(sender, instance, created, **kwargs):
    """
    Сигнал: Освобождение резервов при переходе К cancelled из ЛЮБОГО статуса.
    
    Триггер: любой_статус → cancelled
    
    Процесс:
    1. Проверяем что текущий статус = 'cancelled' (или is_negative_end)
    2. Проверяем что предыдущий статус НЕ 'cancelled' (чтобы избежать повторной обработки)
    3. Освобождаем все резервы в статусе 'reserved': status → 'released'
    4. Устанавливаем released_at
    
    ПРИМЕРЫ сценариев:
    - draft → cancelled: резервы 'reserved' → 'released' ✅
    - pending → cancelled: резервы 'reserved' → 'released' ✅
    - completed → cancelled: обрабатывается rollback_sale_on_status_change ⚠️
    
    ПРИМЕЧАНИЕ: Для completed → cancelled резервы в 'converted_to_sale',
    поэтому этот сигнал их не затронет (обрабатывает rollback_sale_on_status_change).
    """
    import logging
    logger = logging.getLogger(__name__)
    
    # Пропускаем новые заказы
    if created:
        return
    
    # Проверяем наличие статуса
    if not instance.status:
        return
    
    current_status = instance.status
    
    # Проверяем: это статус отмены?
    if not current_status.is_negative_end:
        return  # Не отмена, выходим

    # === Получаем предыдущий статус из pre_save сигнала ===
    previous_status = getattr(instance, '_previous_status', None)

    # Проверяем: не был ли уже в cancelled?
    if previous_status and previous_status.is_negative_end:
        return  # Уже был в отмене, не обрабатываем повторно
    
    # === Освобождаем резервы ===
    # Ищем только резервы в статусе 'reserved'
    # Резервы в 'converted_to_sale' обрабатывает rollback_sale_on_status_change
    reservations = Reservation.objects.filter(
        order_item__order=instance,
        status='reserved'
    )
    
    # Исключаем витринные временные комплекты - они остаются на витрине
    showcase_kit_reservations = reservations.filter(
        product_kit__is_temporary=True,
        product_kit__showcase__isnull=False
    )
    
    # Освобождаем только обычные резервы
    normal_reservations = reservations.exclude(
        id__in=showcase_kit_reservations.values_list('id', flat=True)
    )
    
    normal_count = normal_reservations.count()
    showcase_count = showcase_kit_reservations.count()
    
    if normal_count > 0:
        logger.info(
            f"🔄 Переход к статусу '{current_status.name}' для заказа {instance.order_number}. "
            f"Освобождаем {normal_count} обычных резервов..."
        )
        
        # Обновляем резервы через .save() чтобы сработал сигнал обновления Stock
        for reservation in normal_reservations:
            reservation.status = 'released'
            reservation.released_at = timezone.now()
            reservation.save(update_fields=['status', 'released_at'])
        
        logger.info(
            f"✅ Освобождено {normal_count} обычных резервов: reserved → released"
        )
    
    if showcase_count > 0:
        # Для витринных комплектов очищаем блокировки корзины
        # КРИТИЧНО: Отвязываем резервы от заказа при отмене
        for reservation in showcase_kit_reservations:
            # Сохраняем ID для защиты от кражи резервов другими заказами
            if reservation.order_item:
                reservation.original_order_item_id = reservation.order_item.id
            reservation.order_item = None
            reservation.cart_lock_expires_at = None
            reservation.locked_by_user = None
            reservation.cart_session_id = None
            reservation.save(update_fields=['order_item', 'original_order_item_id',
                                            'cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])
        
        logger.info(
            f"ℹ️ Найдено {showcase_count} резервов витринных комплектов - остаются в reserved (на витрине, отвязаны от заказа, блокировки сняты)"
        )
    
    if normal_count == 0 and showcase_count == 0:
        logger.debug(
            f"ℹ️ Для заказа {instance.order_number} нет резервов в статусе 'reserved'"
        )
    
    # === Освобождаем ShowcaseItem при отмене: reserved/sold → available ===
    from inventory.models import ShowcaseItem
    
    # Находим все ShowcaseItem для этого заказа в статусах reserved или sold
    showcase_items_to_release = ShowcaseItem.objects.filter(
        sold_order_item__order=instance,
        status__in=['reserved', 'sold']
    )
    
    released_showcase_count = 0
    for showcase_item in showcase_items_to_release:
        try:
            showcase_item.return_to_available()
            released_showcase_count += 1
            logger.info(
                f"✓ Витринный экземпляр #{showcase_item.id} освобождён: {showcase_item.status} → available"
            )
        except Exception as e:
            logger.error(
                f"❌ Ошибка освобождения ShowcaseItem #{showcase_item.id}: {e}"
            )
    
    if released_showcase_count > 0:
        logger.info(
            f"🎉 {released_showcase_count} витринных экземпляров освобождено и возвращено на витрину при отмене заказа"
        )
    
    # === Обновляем is_returned ===
    # Используем единую функцию для обновления флага
    update_is_returned_flag(instance)


@receiver(post_save, sender=Order)
@transaction.atomic
def reserve_stock_on_uncancellation(sender, instance, created, **kwargs):
    """
    Сигнал: Резервирование товара при переходе ОТ статуса 'cancelled' к другим статусам.
    
    Триггер: cancelled → любой НЕ отменённый статус (draft, pending и т.д.)
    
    Процесс:
    1. Проверяем что предыдущий статус был 'cancelled' (is_negative_end)
    2. Проверяем что текущий статус НЕ 'cancelled'
    3. Находим резервы в статусе 'released'
    4. Переводим их обратно в 'reserved'
    5. Stock автоматически обновится через сигнал
    
    ПРИМЕРЫ сценариев:
    - cancelled → draft: резервы 'released' → 'reserved' ✅
    - cancelled → pending: резервы 'released' → 'reserved' ✅
    
    ПРИМЕЧАНИЕ: Прямой переход cancelled → completed НЕВОЗМОЖЕН.
    Order.save() автоматически разбивает его на два шага:
    1. cancelled → draft (этот сигнал резервирует)
    2. draft → completed (create_sale_on_order_completion финализирует)
    """
    import logging
    logger = logging.getLogger(__name__)
    
    # Пропускаем новые заказы
    if created:
        return
    
    # Проверяем наличие статуса
    if not instance.status:
        return
    
    current_status = instance.status
    
    # Проверяем: текущий статус НЕ отмена?
    if current_status.is_negative_end:
        return  # Всё ещё в отмене, выходим

    # === Получаем предыдущий статус из pre_save сигнала ===
    previous_status = getattr(instance, '_previous_status', None)
    if not previous_status:
        return  # Нет предыдущего статуса

    # Проверяем: был ли предыдущий статус = cancelled?
    if not previous_status.is_negative_end:
        return  # Не было перехода от cancelled, выходим
    
    # === Резервируем товар заново ===
    # Ищем резервы в статусе 'released' (обычные резервы)
    reservations = Reservation.objects.filter(
        order_item__order=instance,
        status='released'
    )
    
    reservations_count = reservations.count()
    
    if reservations_count > 0:
        logger.info(
            f"🔄 Переход от статуса '{previous_status.name}' к '{current_status.name}' для заказа {instance.order_number}. "
            f"Резервируем {reservations_count} освобождённых резервов..."
        )
        
        # Обновляем резервы через .save() чтобы сработал сигнал обновления Stock
        for reservation in reservations:
            reservation.status = 'reserved'
            reservation.reserved_at = timezone.now()  # Обновляем время резервирования
            reservation.save(update_fields=['status', 'reserved_at'])
        
        logger.info(
            f"✅ Зарезервировано {reservations_count} резервов: released → reserved"
        )
    else:
        logger.debug(
            f"ℹ️ Для заказа {instance.order_number} нет резервов в статусе 'released'"
        )
    
    # === Привязываем витринные резервы обратно к заказу ===
    # Витринные резервы остаются в статусе 'reserved' при отмене,
    # но отвязываются от order_item. При возврате нужно привязать их обратно.
    
    # Находим все OrderItem витринных комплектов в этом заказе
    showcase_order_items = instance.items.filter(
        product_kit__is_temporary=True,
        product_kit__showcase__isnull=False
    ).select_related('product_kit')
    
    showcase_items_count = showcase_order_items.count()
    
    if showcase_items_count > 0:
        logger.info(
            f"🔄 Найдено {showcase_items_count} витринных комплектов в заказе. "
            f"Привязываем резервы обратно к заказу..."
        )
        
        for order_item in showcase_order_items:
            kit = order_item.product_kit

            # КРИТИЧНО: Находим ТОЛЬКО "родные" витринные резервы этого OrderItem
            # Используем original_order_item_id для защиты от кражи резервов другими заказами
            showcase_reservations = Reservation.objects.filter(
                product_kit=kit,
                showcase__isnull=False,
                status='reserved',
                order_item__isnull=True,  # Отвязаны от заказа
                original_order_item_id=order_item.id  # ТОЛЬКО "родные" резервы!
            )

            if showcase_reservations.exists():
                # Привязываем резервы обратно к OrderItem
                # Используем цикл вместо bulk update чтобы очистить original_order_item_id
                updated_count = 0
                for reservation in showcase_reservations:
                    reservation.order_item = order_item
                    reservation.original_order_item_id = None  # Очищаем после привязывания
                    reservation.save(update_fields=['order_item', 'original_order_item_id'])
                    updated_count += 1

                logger.info(
                    f"  ✅ Витринный комплект '{kit.name}': привязано {updated_count} резервов к OrderItem #{order_item.id}"
                )
            else:
                logger.warning(
                    f"  ⚠ Витринный комплект '{kit.name}': не найдено витринных резервов с original_order_item_id={order_item.id}"
                )
        
        logger.info(
            f"✅ Обработано {showcase_items_count} витринных комплектов"
        )
    
    # === Возвращаем ShowcaseItem из available обратно в reserved ===
    # При отмене (cancelled) ShowcaseItem переходит в 'available'.
    # При возврате к нейтральному статусу нужно вернуть в 'reserved'.
    # НО: При переходе в положительный финальный статус - НЕ трогаем!
    # (create_sale_on_order_completion сам переведёт available → sold)
    from inventory.models import ShowcaseItem
    
    # Проверяем: если текущий статус положительный финальный, то пропускаем
    if current_status.is_positive_end:
        logger.info(
            f"🔄 Переход к положительному финальному статусу '{current_status.name}'. "
            f"ShowcaseItem остаются в 'available' для финализации в create_sale_on_order_completion."
        )
    else:
        # Переход к нейтральному статусу - возвращаем в reserved
        # Находим все ShowcaseItem которые были освобождены при отмене
        # (у них sold_order_item сброшен в return_to_available, нужно найти через резервы)
        for order_item in showcase_order_items:
            kit = order_item.product_kit
            
            # Находим ShowcaseItem этого комплекта в статусе 'available'
            # Их sold_order_item = None, поэтому ищем через product_kit
            available_items = ShowcaseItem.objects.filter(
                product_kit=kit,
                status='available',
                sold_order_item__isnull=True
            )
            
            if available_items.exists():
                logger.info(
                    f"  🔄 Найдено {available_items.count()} ShowcaseItem комплекта '{kit.name}' в статусе 'available'. "
                    f"Возвращаем в reserved..."
                )
                
                for item in available_items:
                    try:
                        item.return_to_reserved(order_item)
                        logger.info(
                            f"    ✅ ShowcaseItem #{item.id}: available → reserved (привязан к OrderItem #{order_item.id})"
                        )
                    except Exception as e:
                        logger.error(
                            f"    ❌ Ошибка возврата ShowcaseItem #{item.id} в reserved: {e}"
                        )


@receiver(pre_delete, sender=Order)
@transaction.atomic
def release_stock_on_order_delete(sender, instance, **kwargs):
    """
    Сигнал: При удалении/отмене заказа освободить резервы.

    Процесс:
    1. Ищем все резервы для этого заказа ДО удаления
    2. Освобождаем резервы ПОСЛЕ успешного коммита транзакции (кроме витринных комплектов)
    3. Это гарантирует, что резервы освободятся только если удаление успешно
    
    ИСКЛЮЧЕНИЕ: Витринные временные комплекты (is_temporary=True, showcase!=null)
    остаются в статусе 'reserved' даже при удалении заказа.
    """
    # Находим все резервы для этого заказа ДО удаления
    # Используем list() чтобы выполнить запрос сейчас, пока Order ещё существует
    all_reservations = list(
        Reservation.objects.filter(
            order_item__order=instance,
            status='reserved'
        ).select_related('product_kit')
    )
    
    # Разделяем на витринные комплекты и обычные резервы
    showcase_reservations = [
        r for r in all_reservations
        if r.product_kit and r.product_kit.is_temporary and r.product_kit.showcase
    ]
    
    normal_reservations = [
        r for r in all_reservations
        if r not in showcase_reservations
    ]

    # Освобождаем резервы СРАЗУ в pre_delete (до каскадного удаления OrderItem)
    # Это предотвращает ошибку FK constraint при попытке сохранить резерв после удаления OrderItem
    for res in normal_reservations:
        res.status = 'released'
        res.released_at = timezone.now()
        res.order_item = None  # Обнуляем ссылку на удаляемый OrderItem
        res.save()

    # Витринные комплекты остаются зарезервированными, но отвязываем от заказа и блокировки корзины
    for res in showcase_reservations:
        res.order_item = None  # Обнуляем ссылку на удаляемый OrderItem
        res.cart_lock_expires_at = None
        res.locked_by_user = None
        res.cart_session_id = None
        res.save(update_fields=['order_item', 'cart_lock_expires_at', 'locked_by_user', 'cart_session_id'])


@receiver(post_save, sender=OrderItem)
@transaction.atomic
def update_reservation_on_item_change(sender, instance, created, **kwargs):
    """
    Сигнал: При изменении позиции заказа управляем резервами.

    ВАЖНО: При СОЗДАНИИ OrderItem резервы создаются сигналом reserve_stock_on_item_create.
    Этот сигнал обрабатывает только ИЗМЕНЕНИЯ существующих OrderItem.

    Процесс:
    1. Ищем существующий резерв для этой позиции (в ЛЮБОМ статусе)
    2. Если резерв ЕСТЬ - обновляем ТОЛЬКО количество (статус НЕ меняем!)
    3. Если резерва НЕТ - создаем новый (только для обновлений)

    Покрывает сценарии:
    - Редактирование + добавление товаров → создаёт резервы для новых
    - Изменение количества → обновляет резервы (даже если уже converted_to_sale)
    - Витринные комплекты → привязывает существующие витринные резервы к заказу

    КРИТИЧНО: Убран фильтр status='reserved' чтобы избежать дубликатов!
    Резерв ищется по order_item независимо от статуса.
    Это предотвращает создание нового резерва для заказа в статусе 'completed'.
    """
    import logging
    logger = logging.getLogger(__name__)

    # КРИТИЧНО: Пропускаем создание - обрабатывается reserve_stock_on_item_create
    # Это предотвращает дублирование резервов при создании OrderItem
    if created:
        return
    
    # Проверяем, это витринный комплект или обычный товар/комплект
    is_showcase_kit = instance.product_kit and instance.product_kit.is_temporary and instance.product_kit.showcase
    
    if is_showcase_kit:
        # Витринный комплект - привязываем существующие витринные резервы к заказу
        kit = instance.product_kit
        
        # Находим все витринные резервы для компонентов этого комплекта
        showcase_reservations = Reservation.objects.filter(
            product_kit=kit,
            showcase__isnull=False,
            status='reserved'
        )
        
        if showcase_reservations.exists():
            # Привязываем витринные резервы к позиции заказа
            # Используем цикл вместо bulk update чтобы сработали сигналы обновления Stock
            updated_count = 0
            for reservation in showcase_reservations:
                reservation.order_item = instance
                reservation.save(update_fields=['order_item'])
                updated_count += 1

            logger.info(
                f"✓ Витринный комплект '{kit.name}': привязано {updated_count} резервов к OrderItem #{instance.id}, "
                f"заказ {instance.order.order_number}"
            )
        else:
            logger.warning(
                f"⚠ Витринный комплект '{kit.name}': не найдено активных витринных резервов для OrderItem #{instance.id}"
            )
        return  # Для витринных комплектов не создаем новые резервы
    
    # Обычный товар или постоянный комплект
    # Ищем резервы для этой позиции в ЛЮБОМ статусе (не только 'reserved')
    # КРИТИЧНО: Убран фильтр status='reserved' для предотвращения дубликатов
    reservations = Reservation.objects.filter(order_item=instance)

    if reservations.exists():
        if instance.product:
            # Обычный товар - один резерв, обновляем количество напрямую
            reservation = reservations.first()
            old_quantity = reservation.quantity
            
            # Обновляем quantity и quantity_base
            reservation.quantity = Decimal(str(instance.quantity))
            reservation.quantity_base = instance.quantity_in_base_units or Decimal(str(instance.quantity))
            reservation.sales_unit = instance.sales_unit
            reservation.save(update_fields=['quantity', 'quantity_base', 'sales_unit'])

            logger.info(
                f"✓ Резерв #{reservation.id} обновлён: quantity {old_quantity} → {reservation.quantity}, "
                f"quantity_base → {reservation.quantity_base} "
                f"(статус: {reservation.status}, OrderItem #{instance.id}, заказ {instance.order.order_number})"
            )

        elif instance.product_kit and instance.kit_snapshot:
            # Комплект - несколько резервов (по одному на компонент)
            # Обновляем количество каждого резерва пропорционально изменению количества комплектов
            from collections import defaultdict

            # Собираем ожидаемые количества компонентов
            product_quantities = defaultdict(Decimal)
            for kit_item in instance.kit_snapshot.items.select_related('original_product'):
                if kit_item.original_product:
                    product_quantities[kit_item.original_product_id] = (
                        kit_item.quantity * Decimal(str(instance.quantity))
                    )

            # Обновляем каждый резерв
            for reservation in reservations:
                expected_qty = product_quantities.get(reservation.product_id, Decimal('0'))
                if expected_qty > 0:
                    old_quantity = reservation.quantity
                    # Компоненты комплекта всегда в базовых единицах
                    reservation.quantity = expected_qty
                    reservation.quantity_base = expected_qty
                    reservation.save(update_fields=['quantity', 'quantity_base'])

                    logger.info(
                        f"✓ Резерв #{reservation.id} ({reservation.product.name}) обновлён: "
                        f"quantity {old_quantity} → {reservation.quantity} "
                        f"(статус: {reservation.status}, OrderItem #{instance.id})"
                    )
    else:
        # Резерва нет - создаем новый
        from collections import defaultdict

        warehouse = instance.order.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()

        if not warehouse:
            logger.warning(f"⚠ Не найден склад для резервирования (OrderItem #{instance.id})")
            return

        if instance.product:
            # Обычный товар - создаем один резерв
            reservation = Reservation.objects.create(
                order_item=instance,
                product=instance.product,
                warehouse=warehouse,
                quantity=Decimal(str(instance.quantity)),
                status='reserved'
            )

            logger.info(
                f"✓ Создан новый резерв #{reservation.id}: {instance.product.name}, quantity={reservation.quantity} "
                f"(OrderItem #{instance.id}, заказ {instance.order.order_number})"
            )

        elif instance.product_kit and instance.kit_snapshot:
            # Обычный комплект - резервируем КОМПОНЕНТЫ из снимка
            # Группируем одинаковые товары
            product_quantities = defaultdict(Decimal)

            for kit_item in instance.kit_snapshot.items.select_related('original_product'):
                if kit_item.original_product:
                    product_quantities[kit_item.original_product_id] += (
                        kit_item.quantity * Decimal(str(instance.quantity))
                    )

            # Создаём резервы для компонентов
            from products.models import Product
            for product_id, total_qty in product_quantities.items():
                product = Product.objects.get(pk=product_id)
                _create_or_update_reservation(
                    instance, product, warehouse, total_qty, product_kit=instance.product_kit
                )

            logger.info(
                f"✓ Комплект '{instance.product_kit.name}': создано {len(product_quantities)} резервов компонентов "
                f"(OrderItem #{instance.id}, заказ {instance.order.order_number})"
            )


# Сигналы для Incoming удалены - теперь StockBatch создается напрямую в IncomingDocumentService


@receiver(post_save, sender=Sale)
def process_sale_fifo(sender, instance, created, **kwargs):
    """
    Сигнал: При создании продажи (Sale) автоматически применяется FIFO-списание.

    Процесс:
    1. Проверяем, новая ли продажа
    2. Если уже обработана - пропускаем
    3. Списываем товар по FIFO из партий
    4. Создаем SaleBatchAllocation для аудита
    """
    if not created:
        return  # Только для новых продаж

    # Если уже обработана - пропускаем
    if instance.processed:
        return

    # Списываем товар по FIFO с allow_negative=True для поддержки продаж "в минус"
    allocations, pending = StockBatchManager.write_off_by_fifo(
        instance.product,
        instance.warehouse,
        instance.quantity,
        allow_negative=True
    )

    # Фиксируем распределение для аудита
    for batch, qty_allocated in allocations:
        SaleBatchAllocation.objects.create(
            sale=instance,
            batch=batch,
            quantity=qty_allocated,
            cost_price=batch.cost_price
        )

    # Если есть pending - это продажа "в минус"
    update_fields = ['processed']
    instance.processed = True
    if pending > 0:
        instance.is_pending_cost = True
        instance.pending_quantity = pending
        update_fields.extend(['is_pending_cost', 'pending_quantity'])

    # Отмечаем продажу как обработанную (используем update() чтобы избежать повторного срабатывания сигнала)
    Sale.objects.filter(pk=instance.pk).update(**{field: getattr(instance, field) for field in update_fields})

    # Stock уже обновлен в StockBatchManager.write_off_by_fifo() → refresh_stock_cache()
    # Не нужно вызывать ещё раз чтобы избежать race condition


@receiver(pre_delete, sender=Sale)
def update_order_on_sale_delete(sender, instance, **kwargs):
    """
    Обновляет флаг is_returned заказа при удалении Sale.
    Вызывается ДО удаления, чтобы можно было получить order.
    """
    if instance.order:
        # Сохраняем order_id для использования в post_delete
        # (instance.order может быть недоступен после удаления)
        instance._order_for_update = instance.order


@receiver(post_delete, sender=Sale)
def update_order_on_sale_post_delete(sender, instance, **kwargs):
    """
    Обновляет флаг is_returned заказа ПОСЛЕ удаления Sale.
    Использует order, сохранённый в pre_delete.
    """
    order = getattr(instance, '_order_for_update', None)
    if order:
        # Обновляем флаг напрямую после удаления Sale
        update_is_returned_flag(order)


# Сигнал process_inventory_reconciliation удален
# Теперь обработка инвентаризации вызывается явно через InventoryCompleteView
# Это позволяет пользователю контролировать момент создания документов


@receiver(post_save, sender=WriteOff)
def update_stock_on_writeoff(sender, instance, created, **kwargs):
    """
    Сигнал: При создании или изменении WriteOff (списание) обновляем Stock.

    Процесс:
    1. При создании списания - товар удаляется из StockBatch
    2. Обновляем запись Stock для этого товара
    """
    # Получаем или создаем Stock запись
    stock, _ = Stock.objects.get_or_create(
        product=instance.batch.product,
        warehouse=instance.batch.warehouse
    )

    # Пересчитываем остаток из всех активных партий
    # refresh_from_batches() уже вызывает save()
    stock.refresh_from_batches()


@receiver(post_save, sender=Reservation)
@transaction.atomic
def update_stock_on_reservation_change(sender, instance, created, **kwargs):
    """
    Сигнал: При создании или изменении резерва (Reservation) обновляем Stock.

    ОПТИМИЗАЦИЯ: Обновляем Stock только если изменились поля, влияющие на резервы:
    - status (reserved/converted_to_sale/released)
    - quantity (количество резерва в единицах продажи)
    - quantity_base (количество резерва в базовых единицах)
    - created (новый резерв)

    Процесс:
    1. При создании резерва пересчитываем quantity_reserved
    2. При изменении статуса или количества пересчитываем quantity_reserved
    3. При других изменениях (например, дата) НЕ пересчитываем Stock (оптимизация)
    """
    if not instance.product or not instance.warehouse:
        return

    # Если это создание нового резерва - всегда обновляем Stock
    if created:
        stock, _ = Stock.objects.get_or_create(
            product=instance.product,
            warehouse=instance.warehouse
        )
        stock.refresh_from_batches()
        return

    # Для обновления - проверяем, изменились ли поля, влияющие на Stock
    # Используем django-simple-history или проверяем через update_fields
    update_fields = kwargs.get('update_fields', None)
    
    # Если update_fields указаны (вызов через save(update_fields=[...]))
    # проверяем, есть ли среди них 'status', 'quantity' или 'quantity_base'
    if update_fields is not None:
        fields_affecting_stock = {'status', 'quantity', 'quantity_base'}
        if not fields_affecting_stock.intersection(update_fields):
            # Изменились другие поля (например, дата) - не обновляем Stock
            return
    
    # Если дошли сюда - нужно обновить Stock
    stock, _ = Stock.objects.get_or_create(
        product=instance.product,
        warehouse=instance.warehouse
    )
    stock.refresh_from_batches()


@receiver(post_delete, sender=Reservation)
@transaction.atomic
def update_stock_on_reservation_delete(sender, instance, **kwargs):
    """
    Сигнал: При удалении резерва (Reservation) обновляем Stock.

    Процесс:
    1. После удаления резерва пересчитываем quantity_reserved
    2. Обновляем запись Stock для этого товара
    """
    if not instance.product or not instance.warehouse:
        return

    try:
        # Получаем Stock запись (не создаем новую при удалении)
        stock = Stock.objects.get(
            product=instance.product,
            warehouse=instance.warehouse
        )

        # Пересчитываем остатки из всех активных партий и резервов
        # refresh_from_batches() уже вызывает save()
        stock.refresh_from_batches()

    except Stock.DoesNotExist:
        # Если Stock записи нет - ничего не делаем
        pass


def _update_product_in_stock(product_id):
    """
    Вспомогательная функция: обновить статус in_stock для товара на основе остатков.
    Товар считается в наличии, если существует хотя бы одна Stock запись
    с положительным quantity_available (free quantity).
    """
    from products.models import Product

    try:
        product = Product.objects.get(id=product_id)

        # Проверяем есть ли остаток где-нибудь на складе
        # Товар в наличии если есть хотя бы один Stock с положительным quantity_available
        has_stock = Stock.objects.filter(
            product=product,
            quantity_available__gt=0
        ).exists()

        # Обновляем in_stock если изменился статус
        if product.in_stock != has_stock:
            product.in_stock = has_stock
            # Обновляем без повторного срабатывания сигналов
            Product.objects.filter(id=product.id).update(in_stock=has_stock)

    except Product.DoesNotExist:
        pass


@receiver(post_save, sender=Stock)
def update_product_in_stock_on_stock_change(sender, instance, created, **kwargs):
    """
    Сигнал: При изменении остатков (Stock) обновляем Product.in_stock.

    Процесс:
    1. После обновления Stock проверяем наличие товара
    2. Если есть положительный остаток - в_наличии=True
    3. Если нет остатков - в_наличии=False
    """
    _update_product_in_stock(instance.product_id)


@receiver(post_delete, sender=Stock)
def update_product_in_stock_on_stock_delete(sender, instance, **kwargs):
    """
    Сигнал: При удалении Stock записи обновляем Product.in_stock.
    Используем post_delete чтобы правильно проверить остались ли ещё Stock записи.
    """
    product_id = instance.product_id
    _update_product_in_stock(product_id)


# ============================================================================
# Сигналы для автоматического обновления себестоимости товара (cost_price)
# ============================================================================


@receiver(post_save, sender=StockBatch)
def update_product_cost_on_batch_change(sender, instance, created, **kwargs):
    """
    Сигнал: При создании или изменении партии (StockBatch) автоматически
    обновляется себестоимость товара (Product.cost_price).

    Процесс:
    1. Проверяем, есть ли связанный товар
    2. Вызываем ProductCostCalculator для пересчета средневзвешенной стоимости
    3. Обновляем поле cost_price в БД

    Триггеры:
    - Создание новой партии (поступление товара)
    - Изменение количества в партии
    - Изменение стоимости партии
    """
    if not instance.product:
        return

    # Импортируем здесь чтобы избежать circular import
    from products.services.cost_calculator import ProductCostCalculator

    try:
        # Пересчитываем и обновляем себестоимость товара
        ProductCostCalculator.update_product_cost(instance.product, save=True)
    except Exception as e:
        import logging
        logger = logging.getLogger(__name__)
        logger.error(
            f"Ошибка при обновлении себестоимости товара {instance.product.sku} "
            f"после изменения партии {instance.id}: {e}",
            exc_info=True
        )


@receiver(post_delete, sender=StockBatch)
def update_product_cost_on_batch_delete(sender, instance, **kwargs):
    """
    Сигнал: При удалении партии (StockBatch) автоматически
    обновляется себестоимость товара.

    Процесс:
    1. После удаления партии пересчитываем себестоимость
    2. Если партий не осталось - cost_price становится 0.00
    """
    if not instance.product:
        return

    # Импортируем здесь чтобы избежать circular import
    from products.services.cost_calculator import ProductCostCalculator

    try:
        # Пересчитываем и обновляем себестоимость товара
        ProductCostCalculator.update_product_cost(instance.product, save=True)
    except Exception as e:
        import logging
        logger = logging.getLogger(__name__)
        logger.error(
            f"Ошибка при обновлении себестоимости товара после удаления партии: {e}",
            exc_info=True
        )


# ============================================================================
# Сигналы для динамического пересчета цен комплектов
# ============================================================================


@receiver(post_save, sender='products.Product')
def update_kit_prices_on_product_change(sender, instance, created, **kwargs):
    """
    Сигнал: При изменении цены товара (price или sale_price)
    автоматически пересчитываются цены всех комплектов, где используется этот товар.

    Процесс:
    1. Находим все KitItem с этим товаром
    2. Для каждого комплекта вызываем recalculate_base_price()
    3. base_price и price обновляются в БД

    Триггеры:
    - Изменение price (основная цена товара)
    - Изменение sale_price (цена со скидкой товара)
    """
    from products.models import KitItem

    # Если это создание товара (не обновление), нет комплектов для пересчета
    if created:
        return

    # Находим все KitItem с этим товаром, исключая временные (витринные) комплекты
    # Витринные комплекты имеют зафиксированную цену и не должны обновляться автоматически
    kit_items = KitItem.objects.filter(
        product=instance
    ).select_related('kit').exclude(
        kit__is_temporary=True
    )

    if not kit_items.exists():
        return  # Товар не используется в комплектах

    # Для каждого комплекта пересчитываем цены
    kits_to_update = set()
    for item in kit_items:
        kits_to_update.add(item.kit_id)

    # Обновляем цены каждого комплекта
    from products.models import ProductKit
    for kit_id in kits_to_update:
        try:
            kit = ProductKit.objects.get(id=kit_id)
            kit.recalculate_base_price()
        except ProductKit.DoesNotExist:
            pass
        except Exception as e:
            import logging
            logger = logging.getLogger(__name__)
            logger.error(
                f"Ошибка при пересчете цены комплекта {kit_id} "
                f"после изменения цены товара {instance.sku}: {e}",
                exc_info=True
            )


# ==================== WRITEOFF DOCUMENT SIGNALS ====================

@receiver(pre_delete, sender=WriteOffDocumentItem)
def release_reservation_on_writeoff_item_delete(sender, instance, **kwargs):
    """
    Сигнал: При удалении позиции документа списания освобождаем связанный резерв.

    Это fallback для случаев удаления напрямую через ORM/Admin,
    минуя WriteOffDocumentService.remove_item().
    """
    if instance.reservation and instance.reservation.status == 'reserved':
        instance.reservation.status = 'released'
        instance.reservation.released_at = timezone.now()
        instance.reservation.save(update_fields=['status', 'released_at'])


# ==================== TRANSFORMATION SIGNALS ====================

@receiver(post_save, sender=TransformationInput)
def reserve_on_transformation_input_create(sender, instance, created, **kwargs):
    """
    При создании входного товара в черновике - резервируем его.
    """
    # Резервируем только при создании нового входного товара
    if not created:
        return

    # Резервируем только если трансформация в draft
    if instance.transformation.status != 'draft':
        return

    # Создаем резерв
    Reservation.objects.create(
        transformation_input=instance,
        product=instance.product,
        warehouse=instance.transformation.warehouse,
        quantity=instance.quantity,
        status='reserved'
    )


@receiver(pre_delete, sender=TransformationInput)
def release_reservation_on_input_delete(sender, instance, **kwargs):
    """
    При удалении входного товара - освобождаем резерв через .save() для обновления Stock.
    """
    reservations = Reservation.objects.filter(
        transformation_input=instance
    )
    for reservation in reservations:
        reservation.status = 'released'
        reservation.released_at = timezone.now()
        reservation.save(update_fields=['status', 'released_at'])


@receiver(post_save, sender=Transformation)
@transaction.atomic
def process_transformation_on_complete(sender, instance, created, **kwargs):
    """
    При переходе в статус 'completed':
    1. FIFO списываем Input
    2. Создаем партии Output с рассчитанной себестоимостью
    3. Обновляем резервы в 'converted_to_transformation'
    """
    if instance.status != 'completed':
        return

    # Проверяем что уже не обработано
    if instance.outputs.filter(stock_batch__isnull=False).exists():
        return  # Уже проведено

    # 1. Списываем Input по FIFO
    total_input_cost = Decimal('0')

    for trans_input in instance.inputs.all():
        allocations, pending = StockBatchManager.write_off_by_fifo(
            product=trans_input.product,
            warehouse=instance.warehouse,
            quantity_to_write_off=trans_input.quantity,
            exclude_transformation=instance,  # Исключаем резервы этой трансформации
            allow_negative=False  # Трансформация требует наличия товара
        )

        # Суммируем себестоимость списанного
        for batch, qty in allocations:
            total_input_cost += batch.cost_price * qty

        # Обновляем резерв через .save() чтобы сработали сигналы обновления Stock
        reservations = Reservation.objects.filter(
            transformation_input=trans_input,
            status='reserved'
        )

        for reservation in reservations:
            reservation.status = 'converted_to_transformation'
            reservation.converted_at = timezone.now()
            reservation.save(update_fields=['status', 'converted_at'])

    # 2. Создаем партии Output
    for trans_output in instance.outputs.all():
        # Рассчитываем себестоимость: сумма Input / количество Output
        if trans_output.quantity > 0:
            output_cost_price = total_input_cost / trans_output.quantity
        else:
            output_cost_price = Decimal('0')

        # Создаем партию
        batch = StockBatchManager.create_batch(
            product=trans_output.product,
            warehouse=instance.warehouse,
            quantity=trans_output.quantity,
            cost_price=output_cost_price
        )

        # Сохраняем ссылку на партию
        trans_output.stock_batch = batch
        trans_output.save(update_fields=['stock_batch'])


@receiver(post_save, sender=Transformation)
@transaction.atomic
def rollback_transformation_on_cancel(sender, instance, **kwargs):
    """
    При отмене проведенной трансформации:
    1. Удаляем партии Output
    2. Восстанавливаем партии Input (обратное FIFO списание)
    3. Возвращаем резервы в 'reserved'
    """
    if instance.status != 'cancelled':
        return

    # Проверяем что была проведена (есть партии Output)
    if not instance.outputs.filter(stock_batch__isnull=False).exists():
        # Это был черновик - обрабатывается другим сигналом
        return

    # 1. Удаляем партии Output
    for trans_output in instance.outputs.all():
        if trans_output.stock_batch:
            # Сохраняем ссылки на товар и склад перед удалением партии
            output_product = trans_output.product
            output_warehouse = instance.warehouse
            batch = trans_output.stock_batch
            batch.delete()
            
            # ВАЖНО: Обновляем Stock после удаления партии Output
            # Сигнал post_delete для StockBatch не обновляет Stock, только себестоимость
            stock, _ = Stock.objects.get_or_create(
                product=output_product,
                warehouse=output_warehouse
            )
            stock.refresh_from_batches()
            
            trans_output.stock_batch = None
            trans_output.save(update_fields=['stock_batch'])

    # 2. Восстанавливаем Input партии
    # УПРОЩЕНИЕ: создаем новые партии с той же себестоимостью что была
    # (в идеале нужно хранить SaleBatchAllocation-подобную таблицу)
    for trans_input in instance.inputs.all():
        # Получаем среднюю себестоимость товара
        cost = trans_input.product.cost_price or Decimal('0')

        # Создаем восстановленную партию
        StockBatchManager.create_batch(
            product=trans_input.product,
            warehouse=instance.warehouse,
            quantity=trans_input.quantity,
            cost_price=cost
        )

        # Возвращаем резерв в reserved через .save() чтобы обновился Stock
        reservations = Reservation.objects.filter(
            transformation_input=trans_input
        )
        for reservation in reservations:
            reservation.status = 'reserved'
            reservation.converted_at = None
            reservation.save(update_fields=['status', 'converted_at'])


@receiver(post_save, sender=Transformation)
def release_reservations_on_draft_cancel(sender, instance, **kwargs):
    """
    При отмене черновика (draft → cancelled) - освобождаем резервы.
    """
    if instance.status != 'cancelled':
        return

    # Проверяем, были ли резервы в статусе 'converted_to_transformation'
    # или имеют заполненное поле converted_at (что означает, что трансформация была проведена)
    # Это работает независимо от порядка выполнения сигналов
    has_converted_reservations = Reservation.objects.filter(
        transformation_input__transformation=instance
    ).filter(
        Q(status='converted_to_transformation') | Q(converted_at__isnull=False)
    ).exists()

    if has_converted_reservations:
        return  # Это была проведенная трансформация, обрабатывается другим сигналом

    # Освобождаем все резервы через .save() чтобы обновился Stock
    reservations = Reservation.objects.filter(
        transformation_input__transformation=instance,
        status='reserved'
    )
    for reservation in reservations:
        reservation.status = 'released'
        reservation.released_at = timezone.now()
        reservation.save(update_fields=['status', 'released_at'])