"""
Менеджер для работы с партиями товаров (StockBatch).
Основной функционал:
- Получение партий для FIFO списания
- Создание новых партий при поступлении
- Списание товара по FIFO при продажах и инвентаризации
"""

from decimal import Decimal
from django.db import transaction
from django.db.models import Sum, Q

from inventory.models import StockBatch, Stock, SaleBatchAllocation


class StockBatchManager:
    """
    Менеджер для работы с партиями товаров.
    Реализует логику FIFO для списания товаров.
    """

    @staticmethod
    def get_batches_for_fifo(product, warehouse, lock=False):
        """
        Получить все активные партии товара на складе для FIFO списания.
        Возвращает ВСЕ партии с quantity > 0, отсортированные по created_at.
        ВАЖНО: Логика учета резервов реализована в write_off_by_fifo().

        Args:
            product: объект Product
            warehouse: объект Warehouse
            lock: bool - использовать ли select_for_update() для блокировки строк
                  (защита от race condition при параллельном списании)

        Returns:
            QuerySet отсортированных партий
        """
        queryset = StockBatch.objects.filter(
            product=product,
            warehouse=warehouse,
            is_active=True,
            quantity__gt=0  # Только партии с остатком
        ).order_by('created_at')  # FIFO: старые первыми
        
        if lock:
            # Блокируем строки для предотвращения race condition
            queryset = queryset.select_for_update()
        
        return queryset

    @staticmethod
    def create_batch(product, warehouse, quantity, cost_price):
        """
        Создать новую партию товара при поступлении.

        Args:
            product: объект Product
            warehouse: объект Warehouse
            quantity: Decimal - количество товара
            cost_price: Decimal - закупочная цена

        Returns:
            Созданный объект StockBatch
        """
        if quantity <= 0:
            raise ValueError("Количество должно быть больше нуля")

        batch = StockBatch.objects.create(
            product=product,
            warehouse=warehouse,
            quantity=quantity,
            cost_price=cost_price
        )

        # Обновляем кеш остатков
        StockBatchManager.refresh_stock_cache(product, warehouse)

        return batch

    @staticmethod
    def write_off_by_fifo(product, warehouse, quantity_to_write_off, exclude_order=None, exclude_transformation=None, allow_negative=False):
        """
        Списать товар по FIFO (старые партии первыми).
        ВАЖНО: Учитывает зарезервированное количество товара.

        Args:
            product: объект Product
            warehouse: объект Warehouse
            quantity_to_write_off: Decimal - сколько списать
            exclude_order: (опционально) объект Order - исключить резервы этого заказа из расчёта.
                           Используется при переводе заказа в 'completed', когда резервы
                           заказа ещё не переведены в 'converted_to_sale'.
            exclude_transformation: (опционально) объект Transformation - исключить резервы этой трансформации из расчёта.
                           Используется при переводе трансформации в 'completed', когда резервы
                           трансформации ещё не переведены в 'converted_to_transformation'.
            allow_negative: (опционально) bool - разрешить продажи "в минус".
                           Если True и товара не хватает, возвращает pending_quantity вместо исключения.

        Returns:
            tuple: (allocations, pending_quantity)
                - allocations: [(batch, qty_written), ...] - какие партии и сколько списано
                - pending_quantity: Decimal - сколько не удалось списать (для продаж "в минус")

        Raises:
            ValueError: если недостаточно свободного товара на складе и allow_negative=False
        """
        from inventory.models import Reservation

        remaining = quantity_to_write_off
        allocations = []

        # Получаем общее количество зарезервированного товара (все резервы со статусом 'reserved')
        # Исключаем резервы заказа или трансформации, для которых делается списание (если переданы)
        reservation_filter = Reservation.objects.filter(
            product=product,
            warehouse=warehouse,
            status='reserved'
        )
        
        # Специальная обработка для трансформации: нужно списывать из зарезервированного товара трансформации
        transformation_reserved_qty = Decimal('0')
        if exclude_transformation:
            transformation_reservations = Reservation.objects.filter(
                product=product,
                warehouse=warehouse,
                status='reserved',
                transformation_input__transformation=exclude_transformation
            )
            transformation_reserved_qty = transformation_reservations.aggregate(total=Sum('quantity_base'))['total'] or Decimal('0')
            # Исключаем резервы трансформации из общего расчета резервов
            reservation_filter = reservation_filter.exclude(transformation_input__transformation=exclude_transformation)
        
        if exclude_order:
            reservation_filter = reservation_filter.exclude(order_item__order=exclude_order)

        total_reserved = reservation_filter.aggregate(total=Sum('quantity_base'))['total'] or Decimal('0')

        # Получаем партии по FIFO с блокировкой строк
        # Используем select_for_update() для предотвращения race condition
        # при параллельном списании из одной партии
        batches = StockBatchManager.get_batches_for_fifo(product, warehouse, lock=True)

        # Проходим партии, списывая товар
        # Если есть exclude_transformation, сначала списываем из зарезервированного товара трансформации
        reserved_remaining = total_reserved  # Сколько резерва (кроме трансформации) еще не распределено по партиям
        transformation_reserved_remaining = transformation_reserved_qty  # Сколько резерва трансформации еще не распределено

        for batch in batches:
            if remaining <= 0:
                break

            # Определяем сколько в этой партии зарезервировано (пропорционально)
            # Логика: старые партии "съедают" резерв первыми (как и при списании)
            batch_reserved_other = min(batch.quantity, reserved_remaining)
            reserved_remaining -= batch_reserved_other
            
            # Если есть резерв трансформации, распределяем его по партиям
            batch_reserved_transformation = Decimal('0')
            if transformation_reserved_remaining > 0:
                # Распределяем резерв трансформации по партиям
                batch_reserved_transformation = min(batch.quantity - batch_reserved_other, transformation_reserved_remaining)
                transformation_reserved_remaining -= batch_reserved_transformation
            
            # Общее зарезервированное в партии
            batch_reserved = batch_reserved_other + batch_reserved_transformation
            
            # Свободное количество в партии
            batch_free = batch.quantity - batch_reserved

            # Если есть резерв трансформации в этой партии, списываем из него
            if batch_reserved_transformation > 0:
                # Списываем из зарезервированного товара трансформации
                qty_from_reserved = min(batch_reserved_transformation, remaining)
                batch.quantity -= qty_from_reserved
                batch.save(update_fields=['quantity', 'updated_at'])
                remaining -= qty_from_reserved
                allocations.append((batch, qty_from_reserved))
                
                if remaining <= 0:
                    break
                    
                # Если партия опустошена, деактивируем её
                if batch.quantity <= 0:
                    batch.is_active = False
                    batch.save(update_fields=['is_active'])
                    continue

            # Если осталось списать, списываем из свободного
            if batch_free > 0 and remaining > 0:
                qty_from_this_batch = min(batch_free, remaining)

                # Списываем
                batch.quantity -= qty_from_this_batch
                batch.save(update_fields=['quantity', 'updated_at'])

                remaining -= qty_from_this_batch

                # Фиксируем распределение
                allocations.append((batch, qty_from_this_batch))

                # Если партия опустошена, деактивируем её
                if batch.quantity <= 0:
                    batch.is_active = False
                    batch.save(update_fields=['is_active'])

        if remaining > 0:
            if allow_negative:
                # Возвращаем сколько не удалось списать (для продаж "в минус")
                StockBatchManager.refresh_stock_cache(product, warehouse)
                return (allocations, remaining)
            else:
                raise ValueError(
                    f"Недостаточно СВОБОДНОГО товара на складе. "
                    f"Требуется {quantity_to_write_off}, доступно {quantity_to_write_off - remaining}. "
                    f"(Общий резерв: {total_reserved})"
                )

        # Обновляем кеш остатков
        StockBatchManager.refresh_stock_cache(product, warehouse)

        return (allocations, Decimal('0'))

    @staticmethod
    def transfer_batch(batch, to_warehouse, quantity):
        """
        Перенести товар из одной партии на другой склад.
        Сохраняет cost_price партии.

        Args:
            batch: объект StockBatch (источник)
            to_warehouse: объект Warehouse (пункт назначения)
            quantity: Decimal - сколько перенести

        Returns:
            Новый объект StockBatch на целевом складе
        """
        if quantity <= 0:
            raise ValueError("Количество должно быть больше нуля")

        if quantity > batch.quantity:
            raise ValueError(
                f"Недостаточно товара в партии. "
                f"Требуется {quantity}, доступно {batch.quantity}"
            )

        # Уменьшаем исходную партию
        batch.quantity -= quantity
        batch.save(update_fields=['quantity', 'updated_at'])

        # Если исходная партия опустошена, деактивируем
        if batch.quantity <= 0:
            batch.is_active = False
            batch.save(update_fields=['is_active'])

        # Создаем новую партию на целевом складе с той же ценой
        new_batch = StockBatch.objects.create(
            product=batch.product,
            warehouse=to_warehouse,
            quantity=quantity,
            cost_price=batch.cost_price  # Сохраняем цену!
        )

        # Обновляем кеш остатков на обоих складах
        StockBatchManager.refresh_stock_cache(batch.product, batch.warehouse)
        StockBatchManager.refresh_stock_cache(batch.product, to_warehouse)

        return new_batch

    @staticmethod
    def refresh_stock_cache(product, warehouse):
        """
        Пересчитать кеш остатков для товара на складе.
        Обновляет модель Stock с агрегированными данными.

        Args:
            product: объект Product
            warehouse: объект Warehouse
        """
        # Получаем или создаем запись Stock
        stock, created = Stock.objects.get_or_create(
            product=product,
            warehouse=warehouse
        )

        # Обновляем её из батчей
        # refresh_from_batches() уже вызывает save() внутри
        stock.refresh_from_batches()

    @staticmethod
    def get_total_stock(product, warehouse):
        """
        Получить общее доступное количество товара на складе.

        Args:
            product: объект Product
            warehouse: объект Warehouse

        Returns:
            Decimal - количество товара
        """
        total = StockBatch.objects.filter(
            product=product,
            warehouse=warehouse,
            is_active=True
        ).aggregate(total=Sum('quantity'))['total'] or Decimal('0')

        return total

    @staticmethod
    def get_batch_details(warehouse, product=None):
        """
        Получить подробную информацию о партиях на складе.
        Полезно для отчетов.

        Args:
            warehouse: объект Warehouse
            product: (опционально) объект Product для фильтрации

        Returns:
            list: QuerySet партий с деталями
        """
        qs = StockBatch.objects.filter(warehouse=warehouse, is_active=True)

        if product:
            qs = qs.filter(product=product)

        return qs.select_related('product', 'warehouse').order_by('product', 'created_at')

    @staticmethod
    @transaction.atomic
    def close_batch(batch):
        """
        Закрыть партию (например, при окончании срока годности).
        Невозможно списывать из закрытой партии.

        Args:
            batch: объект StockBatch
        """
        if batch.quantity > 0:
            raise ValueError(f"Невозможно закрыть партию с остатком {batch.quantity}")

        batch.is_active = False
        batch.save(update_fields=['is_active'])

    @staticmethod
    @transaction.atomic
    def transfer_product_by_fifo(product, from_warehouse, to_warehouse, quantity):
        """
        Переместить товар с одного склада на другой по FIFO логике.
        Старые партии перемещаются первыми.

        Args:
            product: объект Product
            from_warehouse: объект Warehouse (источник)
            to_warehouse: объект Warehouse (назначение)
            quantity: Decimal - количество товара для перемещения

        Returns:
            list: [(source_batch, qty_transferred, new_batch), ...]
            список кортежей с исходной партией, количеством и созданной партией

        Raises:
            ValueError: если недостаточно товара на складе-источнике
        """
        # Получаем партии по FIFO (старые первыми)
        allocations = StockBatchManager.get_batches_for_fifo(product, from_warehouse)

        result = []
        remaining = quantity

        for batch in allocations:
            if remaining <= 0:
                break

            # Определяем сколько перемещаем из этой партии
            qty_to_transfer = min(batch.quantity, remaining)

            # Уменьшаем исходную партию
            batch.quantity -= qty_to_transfer
            if batch.quantity <= 0:
                batch.is_active = False
            batch.save(update_fields=['quantity', 'is_active', 'updated_at'])

            # Создаем новую партию на целевом складе с СОХРАНЕНИЕМ cost_price
            new_batch = StockBatch.objects.create(
                product=product,
                warehouse=to_warehouse,
                quantity=qty_to_transfer,
                cost_price=batch.cost_price  # ВАЖНО: сохраняем цену!
            )

            result.append((batch, qty_to_transfer, new_batch))
            remaining -= qty_to_transfer

        # Проверяем что было достаточно товара
        if remaining > 0:
            raise ValueError(
                f"Недостаточно товара '{product.name}' на складе '{from_warehouse.name}'. "
                f"Не хватает {remaining} шт из запрашиваемых {quantity} шт"
            )

        # Обновляем кеш остатков на обоих складах
        StockBatchManager.refresh_stock_cache(product, from_warehouse)
        StockBatchManager.refresh_stock_cache(product, to_warehouse)

        return result
