# from decimal import Decimal, InvalidOperation
# from django_filters.rest_framework import DjangoFilterBackend
# from rest_framework.filters import SearchFilter
# from rest_framework.permissions import IsAuthenticated
# from shop_management.views.data.base import DataRootViewSet
# from shop_management.views.data.pagination import CustomPagination
# from shop_management.serializers.data.purchase import (
#     PurchaseSerializer, Purchase, PurchaseItem
# )
# from shop_management.serializers.data.purchase_payment import PurchasePayment
# from django.db import transaction
# from rest_framework import status
# from rest_framework.response import Response
# from shop_management.models.data.stock import Stock
# from shop_management.models.data.purchase import Purchase, PurchaseItem
# from django.utils import timezone


# class PurchaseViewSet(DataRootViewSet):
#     serializer_class = PurchaseSerializer
#     filter_backends = [DjangoFilterBackend, SearchFilter]
#     filterset_fields = ['shop', 'supplier', 'discount', 'received_amount']
#     search_fields = ['id', 'shop__name', 'supplier__name']
#     pagination_class = CustomPagination

#     def get_queryset(self):
#         return Purchase.objects.filter(shop__owner=self.request.user).order_by('-id')

#     def create(self, request, *args, **kwargs):
#         data = request.data
#         shop_id = data.get('shop')
#         supplier_id = data.get('supplier')
#         discount_value = data.get('discount', 0)
#         received_amount_value = data.get('received_amount', 0)
#         # Currency now comes at purchase level
#         currency_id = data.get('currency')
#         # Use purchase_date from data, or default to now.
#         purchase_date = data.get('purchase_date') or timezone.now()
#         items_data = data.get('items', [])

#         try:
#             discount = Decimal(discount_value)
#         except (InvalidOperation, TypeError):
#             discount = Decimal(0)
#         try:
#             received_amount = Decimal(received_amount_value)
#         except (InvalidOperation, TypeError):
#             received_amount = Decimal(0)

#         with transaction.atomic():
#             try:
#                 purchase = Purchase.objects.create(
#                     shop_id=shop_id,
#                     supplier_id=supplier_id,
#                     discount=discount,
#                     received_amount=received_amount,
#                     currency_id=currency_id,
#                     purchase_date=purchase_date
#                 )
#             except Exception as e:
#                 return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)

#             for item in items_data:
#                 try:
#                     purchase_item = PurchaseItem.objects.create(
#                         purchase=purchase,
#                         product_id=item.get('product'),
#                         unit_id=item.get('unit'),
#                         quantity=item.get('quantity'),
#                         buy_price=item.get('buy_price'),
#                         sale_price=item.get('sale_price'),
#                         expire_date=item.get('expire_date')  # optional
#                     )
#                     # Update stock based on the created purchase item.
#                     self._update_stock_on_create(purchase, purchase_item)
#                 except Exception as e:
#                     transaction.set_rollback(True)
#                     return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)

#             if received_amount > 0:
#                 PurchasePayment.objects.create(
#                     purchase=purchase,
#                     amount=received_amount,
#                     currency_id=currency_id
#                 )

#             return Response({"message": "Purchase created successfully"}, status=status.HTTP_201_CREATED)

#     def update(self, request, *args, **kwargs):
#         pk = kwargs.get('pk')
#         try:
#             purchase = Purchase.objects.get(pk=pk)
#         except Purchase.DoesNotExist:
#             return Response({"error": "Purchase not found"}, status=status.HTTP_404_NOT_FOUND)

#         data = request.data
#         shop_id = data.get('shop', purchase.shop_id)
#         supplier_id = data.get('supplier', purchase.supplier_id)
#         discount_value = data.get('discount', purchase.discount)
#         received_amount_value = data.get('received_amount', purchase.received_amount or 0)
#         try:
#             discount = Decimal(discount_value)
#         except (InvalidOperation, TypeError):
#             discount = Decimal(0)
#         try:
#             received_amount = Decimal(received_amount_value)
#         except (InvalidOperation, TypeError):
#             received_amount = Decimal(0)
#         items_data = data.get('items', [])

#         with transaction.atomic():
#             purchase.shop_id = shop_id
#             purchase.supplier_id = supplier_id
#             purchase.discount = discount
#             purchase.received_amount = received_amount
#             purchase.currency_id = data.get('currency', purchase.currency_id)
#             if 'purchase_date' in data:
#                 purchase.purchase_date = data.get('purchase_date')
#             purchase.save()

#             existing_items = {str(item.id): item for item in purchase.purchase_items.all()}
#             new_items_ids = []

#             for item_data in items_data:
#                 item_id = item_data.get('id')
#                 if item_id and str(item_id) in existing_items:
#                     purchase_item = existing_items[str(item_id)]
#                     old_quantity = int(purchase_item.quantity)
#                     new_quantity = int(item_data.get('quantity', purchase_item.quantity))
#                     purchase_item.product_id = item_data.get('product', purchase_item.product_id)
#                     purchase_item.unit_id = item_data.get('unit', purchase_item.unit_id)
#                     purchase_item.buy_price = item_data.get('buy_price', purchase_item.buy_price)
#                     purchase_item.sale_price = item_data.get('sale_price', purchase_item.sale_price)
#                     purchase_item.quantity = new_quantity
#                     if 'expire_date' in item_data:
#                         purchase_item.expire_date = item_data.get('expire_date')
#                     purchase_item.save()
#                     new_items_ids.append(str(item_id))
#                     # Update stock based on quantity difference.
#                     quantity_diff = new_quantity - old_quantity
#                     self._update_stock_on_update(purchase, purchase_item, quantity_diff)
#                 else:
#                     new_quantity = int(item_data.get('quantity'))
#                     purchase_item = PurchaseItem.objects.create(
#                         purchase=purchase,
#                         product_id=item_data.get('product'),
#                         unit_id=item_data.get('unit'),
#                         quantity=new_quantity,
#                         buy_price=item_data.get('buy_price'),
#                         sale_price=item_data.get('sale_price'),
#                         expire_date=item_data.get('expire_date')
#                     )
#                     self._update_stock_on_create(purchase, purchase_item)

#             for item_id, purchase_item in existing_items.items():
#                 if item_id not in new_items_ids:
#                     self._update_stock_on_delete(purchase, purchase_item)
#                     purchase_item.delete()

#             if received_amount > 0:
#                 PurchasePayment.objects.create(
#                     purchase=purchase,
#                     amount=received_amount,
#                     currency_id=purchase.currency_id
#                 )

#             return Response({"message": "Purchase updated successfully"}, status=status.HTTP_200_OK)

#     # ----- Stock update helper methods (updating quantity, sale_price) -----

#     def _update_stock_on_create(self, purchase, purchase_item):
#         new_qty = int(purchase_item.quantity)
#         stocks = Stock.objects.filter(
#             shop=purchase.shop,
#             purchase_item__product=purchase_item.product
#         )
#         if stocks.exists():
#             stock = stocks.first()
#             duplicate_stocks = stocks.exclude(id=stock.id)
#             for dup in duplicate_stocks:
#                 stock.quantity += int(dup.quantity)
#                 dup.delete()
#             stock.quantity += new_qty
#             # Update sale_price from the purchase_item
#             stock.sale_price = purchase_item.sale_price or stock.sale_price
#             stock.save()
#         else:
#             Stock.objects.create(
#                 shop=purchase.shop,
#                 purchase_item=purchase_item,
#                 quantity=new_qty,
#                 sale_price=purchase_item.sale_price or 0.00
#             )

#     def _update_stock_on_update(self, purchase, purchase_item, quantity_diff):
#         try:
#             stocks = Stock.objects.filter(
#                 shop=purchase.shop,
#                 purchase_item__product=purchase_item.product
#             )
#             if stocks.exists():
#                 stock = stocks.first()
#                 duplicate_stocks = stocks.exclude(id=stock.id)
#                 for dup in duplicate_stocks:
#                     stock.quantity += int(dup.quantity)
#                     dup.delete()
#                 stock.quantity += int(quantity_diff)
#                 # Update sale_price from the purchase_item
#                 stock.sale_price = purchase_item.sale_price or stock.sale_price
#                 if stock.quantity <= 0:
#                     stock.delete()
#                 else:
#                     stock.save()
#             else:
#                 if int(purchase_item.quantity) > 0:
#                     Stock.objects.create(
#                         shop=purchase.shop,
#                         purchase_item=purchase_item,
#                         quantity=int(purchase_item.quantity),
#                         sale_price=purchase_item.sale_price or 0.00
#                     )
#         except Exception:
#             pass

#     def _update_stock_on_delete(self, purchase, purchase_item):
#         try:
#             stocks = Stock.objects.filter(
#                 shop=purchase.shop,
#                 purchase_item__product=purchase_item.product
#             )
#             if stocks.exists():
#                 stock = stocks.first()
#                 duplicate_stocks = stocks.exclude(id=stock.id)
#                 for dup in duplicate_stocks:
#                     stock.quantity += int(dup.quantity)
#                     dup.delete()
#                 stock.quantity -= int(purchase_item.quantity)
#                 # Update sale_price from the purchase_item
#                 stock.sale_price = purchase_item.sale_price or stock.sale_price
#                 if stock.quantity <= 0:
#                     stock.delete()
#                 else:
#                     stock.save()
#         except Stock.DoesNotExist:
#             pass


from decimal import Decimal, InvalidOperation
import json

from django.utils import timezone
from django.db import transaction, models
from django_filters.rest_framework import DjangoFilterBackend, FilterSet, DateFilter
from rest_framework import status
from rest_framework.filters import SearchFilter
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from django.core.exceptions import ValidationError as DjangoValidationError
from account.permissions import (
    HasTabPermission, HasActionPermission, assert_shop_access, assert_warehouse_access,
    user_can_see_all_shop_data,
    get_user_data_filter,
)
from rest_framework.response import Response

# Project imports
from shop_management.views.data.base import DataRootViewSet
from shop_management.views.data.pagination import CustomPagination
from shop_management.serializers.data.purchase import (
    PurchaseSerializer,
    Purchase,
    PurchaseItem,
)
from shop_management.models.data.product import Product
from shop_management.models.data.purchase_payment import PurchasePayment
from shop_management.serializers.data.purchase_payment import PurchasePaymentSerializer
from shop_management.models.data.stock import Stock
from shop_management.models.data.inventory import StockMovement
from shop_management.models.data.stock_transfer import StockTransfer
from shop_management.models.data.organization import Warehouse
from shop_management.models.data.supplier import Supplier
from shop_management.models.data.utils import Currency
from shop_management.services.purchases import post_purchase, void_purchase


def optional_fk(value):
    return None if value in ("", None, "null", "undefined") else value


def resolve_purchase_item_products(items_data, shop_id):
    """Return validated products; purchase units always come from product setup."""
    product_ids = [optional_fk(item.get("product")) for item in items_data]
    products = {
        str(product.pk): product
        for product in Product.objects.filter(pk__in=[pk for pk in product_ids if pk]).select_related("default_unit")
    }
    errors = {}
    for index, item in enumerate(items_data):
        product = products.get(str(optional_fk(item.get("product"))))
        if not product:
            errors[f"items[{index}].product"] = "Select a valid product."
        elif str(product.shop_id) != str(shop_id):
            errors[f"items[{index}].product"] = "Product must belong to the selected branch."
        elif not product.default_unit_id:
            errors[f"items[{index}].unit"] = (
                f'Product "{product.name}" has no default unit. Edit the product and select a unit first.'
            )
        elif product.default_unit.shop_id != product.shop_id:
            errors[f"items[{index}].unit"] = "The product default unit does not belong to its branch."
    if errors:
        raise ValidationError(errors)
    return products


def stock_only_transfer_purchase_ids():
    """Purchases created only so Stock can list a Payment-OFF transfer.

    They must not appear as a second buy on the Purchases page.
    Payment-ON "Transfer from …" purchases stay visible (money/debt).
    """
    off_ids = set(
        StockTransfer.objects.filter(
            status=StockTransfer.Status.ACCEPTED,
            show_payment_button=False,
            target_stock__purchase_item__purchase_id__isnull=False,
        ).values_list("target_stock__purchase_item__purchase_id", flat=True)
    )
    on_ids = set(
        StockTransfer.objects.filter(
            status=StockTransfer.Status.ACCEPTED,
            show_payment_button=True,
            target_stock__purchase_item__purchase_id__isnull=False,
        ).values_list("target_stock__purchase_item__purchase_id", flat=True)
    )
    leftover_transfer_from = set(
        Purchase.objects.filter(
            is_deleted=False,
            supplier__name__istartswith="Transfer from ",
        )
        .exclude(id__in=on_ids)
        .values_list("id", flat=True)
    )
    marked_stock_only = set(
        Purchase.objects.filter(
            is_deleted=False,
            notes__startswith="Stock-only transfer ",
        ).values_list("id", flat=True)
    )
    # A later payment-enabled transfer may reuse the same Stock row. In that
    # case target_stock now points at the payable purchase, so it must win over
    # the stale payment-off relationship. The explicit marker preserves the
    # original stock-only document independently of that mutable pointer.
    return list((off_ids - on_ids) | leftover_transfer_from | marked_stock_only)


# ────────────────────────────────────────────────────────────────────────────────
# FilterSet placed **in the same file** so the whole feature lives here.
# ────────────────────────────────────────────────────────────────────────────────
class PurchaseFilter(FilterSet):
    # URL params → ?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD

    start_date = DateFilter(field_name="purchase_date", lookup_expr="gte")
    end_date = DateFilter(field_name="purchase_date", lookup_expr="lte")

    class Meta:
        model = Purchase
        # Keep the other simple filters you already exposed
        fields = {
            "shop": ["exact"],
            "supplier": ["exact"],
            "amount": ["exact"],
            "received_amount": ["exact"],
        }


# ────────────────────────────────────────────────────────────────────────────────
# ViewSet
# ────────────────────────────────────────────────────────────────────────────────
class PurchaseViewSet(DataRootViewSet):
    """
    CRUD + date-range filtering for Purchase objects.
    Clients can now call, e.g.:

        GET /api/purchases?start_date=2025-05-01&end_date=2025-05-31
    """

    serializer_class = PurchaseSerializer
    pagination_class = CustomPagination
    permission_classes = [IsAuthenticated, HasTabPermission, HasActionPermission]
    action_permission_map = {"create": "purchases.create_purchase", "post": "purchases.post_purchase", "void": "purchases.void_purchase"}
    required_tab = "purchases"
    filter_backends = [DjangoFilterBackend, SearchFilter]
    filterset_class = PurchaseFilter  # ← uses the class above
    search_fields = [
        "id",
        "purchase_number",
        "supplier_invoice_number",
        "shop__name",
        "supplier__name",
        "purchase_items__product__name",
        "purchase_items__product__code",
    ]

    # --------------------------------------------------------------------- queryset
    def get_queryset(self):
        hidden_ids = stock_only_transfer_purchase_ids()
        if user_can_see_all_shop_data(self.request.user, self.required_tab):
            qs = Purchase.objects.filter(is_deleted=False)
        else:
            qs = Purchase.objects.filter(
                get_user_data_filter(self.request.user, "shop"),
                is_deleted=False,
            )
        if hidden_ids:
            qs = qs.exclude(id__in=hidden_ids)
        if self.request.query_params.get("return_eligible") == "true":
            qs = qs.filter(
                models.Q(uses_inventory_engine=False)
                | models.Q(document_status=Purchase.DocumentStatus.POSTED)
            )
        qs = (
            qs.select_related("shop", "supplier", "currency", "warehouse")
            .prefetch_related(
                "purchase_items__product",
                "purchase_items__unit",
                "purchase_items__purchase",
                "payments__currency",
            )
            .order_by("-id")
        )
        if not user_can_see_all_shop_data(self.request.user, self.required_tab):
            qs = qs.distinct()
        return qs

    # --------------------------------------------------------------------- create
    def create(self, request, *args, **kwargs):
        data = request.data
        shop_id = data.get("shop")
        supplier_id = optional_fk(data.get("supplier"))
        discount_value = data.get("amount", 0)
        received_amount_value = data.get("received_amount", 0)
        currency_id = data.get("currency")
        purchase_date = data.get("purchase_date") or timezone.now()
        items_data = data.get("items", [])
        warehouse_id = optional_fk(data.get("warehouse"))
        uses_inventory_engine = bool(warehouse_id)

        shop = assert_shop_access(request.user, shop_id, request)
        item_products = resolve_purchase_item_products(items_data, shop.pk)
        if uses_inventory_engine and not supplier_id:
            return Response({"supplier": "Select a supplier before creating this purchase."}, status=status.HTTP_400_BAD_REQUEST)
        if supplier_id and not Supplier.objects.filter(pk=supplier_id, shop_id=shop.pk, is_deleted=False).exists():
            return Response({"supplier": "Select a supplier from the selected branch."}, status=status.HTTP_400_BAD_REQUEST)
        if uses_inventory_engine and not currency_id:
            return Response({"currency": "Select a currency before creating this purchase."}, status=status.HTTP_400_BAD_REQUEST)
        if currency_id and not Currency.objects.filter(pk=currency_id, shop_id=shop.pk).exists():
            return Response({"currency": "Select a currency from the selected branch."}, status=status.HTTP_400_BAD_REQUEST)
        if warehouse_id:
            warehouse = assert_warehouse_access(request.user, warehouse_id, request, active_only=True)
            if warehouse.shop_id != shop.pk:
                raise ValidationError({"warehouse": "Warehouse must belong to the selected branch."})

        if uses_inventory_engine and not Warehouse.objects.filter(
            pk=warehouse_id, shop_id=shop_id, is_active=True
        ).exists():
            return Response(
                {"warehouse": "Select an active warehouse belonging to this shop."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        try:
            discount = Decimal(discount_value)
        except (InvalidOperation, TypeError):
            discount = Decimal(0)

        try:
            received_amount = Decimal(received_amount_value)
        except (InvalidOperation, TypeError):
            received_amount = Decimal(0)

        if uses_inventory_engine and received_amount > 0:
            return Response(
                {"received_amount": "Create the draft first, post it, then record supplier payment."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        with transaction.atomic():
            try:
                purchase = Purchase.objects.create(
                    shop_id=shop_id,
                    supplier_id=supplier_id,
                    amount=discount,
                    received_amount=received_amount,
                    currency_id=currency_id,
                    purchase_date=purchase_date,
                    created_by=request.user,
                    warehouse_id=warehouse_id,
                    supplier_invoice_number=data.get("supplier_invoice_number", ""),
                    due_date=optional_fk(data.get("due_date")),
                    exchange_rate_used=optional_fk(data.get("exchange_rate_used")),
                    notes=data.get("notes", ""),
                    uses_inventory_engine=uses_inventory_engine,
                    document_status=(
                        Purchase.DocumentStatus.DRAFT
                        if uses_inventory_engine
                        else Purchase.DocumentStatus.POSTED
                    ),
                )
            except Exception as e:
                return Response({"error": str(e)}, status=status.HTTP_400_BAD_REQUEST)

            # --- items & stock
            for item in items_data:
                try:
                    retail = item.get("retail_price") or None
                    purchase_item = PurchaseItem.objects.create(
                        purchase=purchase,
                        product_id=item.get("product"),
                        unit_id=item_products[str(item.get("product"))].default_unit_id,
                        quantity=item.get("quantity"),
                        buy_price=item.get("buy_price"),
                        wholesale_price=item.get("wholesale_price") or None,
                        retail_price=retail,
                        # sale_price defaults to retail_price so stock uses it
                        sale_price=retail or item.get("sale_price"),
                        expire_date=item.get("expire_date"),  # optional
                        batch_number=item.get("batch_number", ""),
                        manufacture_date=optional_fk(item.get("manufacture_date")),
                        created_by=request.user,
                    )
                    if not uses_inventory_engine:
                        self._apply_purchase_item_stock_delta(
                            purchase.shop,
                            purchase_item,
                            int(purchase_item.quantity or 0),
                            created_by=purchase.created_by,
                        )
                except Exception as e:
                    transaction.set_rollback(True)
                    return Response(
                        {"error": str(e)}, status=status.HTTP_400_BAD_REQUEST
                    )

            # --- payment
            if received_amount > 0 and not uses_inventory_engine:
                payment_response = self._save_purchase_payment(
                    purchase=purchase,
                    amount=received_amount,
                    data=data,
                    user=request.user,
                )
                if payment_response is not None:
                    transaction.set_rollback(True)
                    return payment_response

            if not uses_inventory_engine:
                purchase.update_status()

            return Response(
                PurchaseSerializer(purchase, context={"request": request}).data,
                status=status.HTTP_201_CREATED,
            )

    # --------------------------------------------------------------------- update
    def update(self, request, *args, **kwargs):
        purchase = self.get_object()

        if purchase._is_stock_transfer_purchase():
            return Response(
                {"detail": "Manage this record from Stock Transfer, not Purchases."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        if purchase.uses_inventory_engine:
            return self._update_engine_draft(request, purchase)

        data = request.data
        old_received_amount = purchase.received_amount or Decimal(0)
        shop_id = data.get("shop", purchase.shop_id)
        supplier_id = optional_fk(data.get("supplier", purchase.supplier_id))
        discount_value = data.get("amount", 0)
        received_amount_value = data.get(
            "received_amount", purchase.received_amount or 0
        )
        items_data = data.get("items", [])

        try:
            discount = Decimal(discount_value)
        except (InvalidOperation, TypeError):
            discount = Decimal(0)

        try:
            received_amount = Decimal(received_amount_value)
        except (InvalidOperation, TypeError):
            received_amount = Decimal(0)

        with transaction.atomic():
            # --- header
            purchase.shop_id = shop_id
            purchase.supplier_id = supplier_id
            purchase.amount = discount
            purchase.received_amount = received_amount
            purchase.currency_id = data.get("currency", purchase.currency_id)
            if "purchase_date" in data:
                purchase.purchase_date = data.get("purchase_date")
            purchase.save()

            # --- items
            existing_items = {
                str(item.id): item for item in purchase.purchase_items.all()
            }
            new_items_ids = []

            for item_data in items_data:
                item_id = item_data.get("id")
                if item_id and str(item_id) in existing_items:
                    # update existing
                    purchase_item = existing_items[str(item_id)]
                    old_product_id = purchase_item.product_id
                    old_quantity = int(purchase_item.quantity)
                    old_unit = purchase_item.unit
                    old_buy_price = purchase_item.buy_price
                    old_wholesale_price = purchase_item.wholesale_price
                    old_retail_price = purchase_item.retail_price
                    old_sale_price = purchase_item.sale_price
                    new_quantity = int(
                        item_data.get("quantity", purchase_item.quantity)
                    )

                    purchase_item.product_id = item_data.get(
                        "product", purchase_item.product_id
                    )
                    purchase_item.unit_id = item_data.get("unit", purchase_item.unit_id)
                    purchase_item.buy_price = item_data.get(
                        "buy_price", purchase_item.buy_price
                    )
                    purchase_item.wholesale_price = item_data.get(
                        "wholesale_price", purchase_item.wholesale_price
                    ) or None
                    retail = item_data.get("retail_price", purchase_item.retail_price) or None
                    purchase_item.retail_price = retail
                    purchase_item.sale_price = retail or item_data.get(
                        "sale_price", purchase_item.sale_price
                    )
                    purchase_item.quantity = new_quantity
                    if "expire_date" in item_data:
                        purchase_item.expire_date = item_data.get("expire_date")
                    purchase_item.save()

                    new_items_ids.append(str(item_id))
                    if old_product_id and old_product_id != purchase_item.product_id:
                        old_product = Product.objects.filter(pk=old_product_id).first()
                        if old_product:
                            old_snapshot = PurchaseItem(
                                purchase=purchase,
                                product=old_product,
                                unit=old_unit,
                                quantity=old_quantity,
                                buy_price=old_buy_price,
                                wholesale_price=old_wholesale_price,
                                retail_price=old_retail_price,
                                sale_price=old_sale_price,
                            )
                            self._apply_purchase_item_stock_delta(
                                purchase.shop,
                                old_snapshot,
                                -old_quantity,
                                created_by=purchase.created_by,
                            )
                        self._apply_purchase_item_stock_delta(
                            purchase.shop,
                            purchase_item,
                            new_quantity,
                            created_by=purchase.created_by,
                        )
                    else:
                        quantity_diff = new_quantity - old_quantity
                        if quantity_diff:
                            self._apply_purchase_item_stock_delta(
                                purchase.shop,
                                purchase_item,
                                quantity_diff,
                                created_by=purchase.created_by,
                            )
                        else:
                            self._sync_purchase_item_stock_prices(purchase.shop, purchase_item)
                else:
                    # create new
                    new_quantity = int(item_data.get("quantity"))
                    retail = item_data.get("retail_price") or None
                    purchase_item = PurchaseItem.objects.create(
                        purchase=purchase,
                        product_id=item_data.get("product"),
                        unit_id=item_data.get("unit"),
                        quantity=new_quantity,
                        buy_price=item_data.get("buy_price"),
                        wholesale_price=item_data.get("wholesale_price") or None,
                        retail_price=retail,
                        sale_price=retail or item_data.get("sale_price"),
                        expire_date=item_data.get("expire_date"),
                    )
                    self._apply_purchase_item_stock_delta(
                        purchase.shop,
                        purchase_item,
                        int(purchase_item.quantity or 0),
                        created_by=purchase.created_by,
                    )

            # delete removed items
            for item_id, purchase_item in existing_items.items():
                if item_id not in new_items_ids:
                    self._apply_purchase_item_stock_delta(
                        purchase.shop,
                        purchase_item,
                        -int(purchase_item.quantity or 0),
                        created_by=purchase.created_by,
                    )
                    purchase_item.delete()

            # payment
            if received_amount > 0:
                payment_response = self._save_purchase_payment(
                    purchase=purchase,
                    amount=received_amount,
                    data=data,
                    user=request.user,
                    instance=self._get_auto_payment(purchase, old_received_amount),
                )
                if payment_response is not None:
                    transaction.set_rollback(True)
                    return payment_response

            purchase.update_status()

            return Response(
                {"message": "Purchase updated successfully"}, status=status.HTTP_200_OK
            )

    # --------------------------------------------------------------------- destroy (hard delete)
    def destroy(self, request, *args, **kwargs):
        purchase = self.get_object()

        if purchase._is_stock_transfer_purchase():
            return Response(
                {"detail": "Delete this record from Stock Transfer, not Purchases."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        if purchase.uses_inventory_engine:
            with transaction.atomic():
                purchase = Purchase.objects.select_for_update().get(pk=purchase.pk)
                if purchase.document_status != Purchase.DocumentStatus.DRAFT:
                    return Response(
                        {"detail": "Posted or cancelled purchases cannot be deleted."},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
                if StockMovement.objects.filter(
                    reference_type__in=("Purchase", "PurchaseVoid"),
                    reference_id=str(purchase.pk),
                ).exists():
                    return Response(
                        {"detail": "This draft cannot be deleted or edited because it already has stock receipt history. Use Post to safely reconcile a complete receipt first."},
                        status=status.HTTP_400_BAD_REQUEST,
                    )
                purchase.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)

        with transaction.atomic():
            payment_ids = list(purchase.payments.values_list("id", flat=True))
            if payment_ids:
                from shop_management.models.data.account_transaction import AccountTransaction

                for payment_id in payment_ids:
                    AccountTransaction.objects.filter(
                        description__startswith=f"Purchase payment #{payment_id} "
                    ).delete()

            purchase_items = list(purchase.purchase_items.all())
            for purchase_item in purchase_items:
                self._apply_purchase_item_stock_delta(
                    purchase.shop,
                    purchase_item,
                    -int(purchase_item.quantity or 0),
                    created_by=purchase.created_by,
                )
                purchase_item.delete()
            
            # Hard delete purchase (this will cascade delete purchase items and payments)
            purchase.delete()

            return Response(
                {"message": "Purchase deleted successfully"}, status=status.HTTP_200_OK
            )

    def _update_engine_draft(self, request, purchase):
        if purchase.document_status != Purchase.DocumentStatus.DRAFT:
            return Response(
                {"detail": "Only draft purchases can be edited."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        data = request.data
        items_data = data.get("items", [])
        shop_id = data.get("shop", purchase.shop_id)
        shop = assert_shop_access(request.user, shop_id, request)
        item_products = resolve_purchase_item_products(items_data, shop_id)
        supplier_id = optional_fk(data.get("supplier", purchase.supplier_id))
        currency_id = optional_fk(data.get("currency", purchase.currency_id))
        if not supplier_id or not Supplier.objects.filter(pk=supplier_id, shop_id=shop.pk, is_deleted=False).exists():
            return Response({"supplier": "Select a supplier from the selected branch."}, status=status.HTTP_400_BAD_REQUEST)
        if not currency_id or not Currency.objects.filter(pk=currency_id, shop_id=shop.pk).exists():
            return Response({"currency": "Select a currency from the selected branch."}, status=status.HTTP_400_BAD_REQUEST)
        warehouse_id = optional_fk(data.get("warehouse", purchase.warehouse_id))
        if not warehouse_id:
            return Response(
                {"warehouse": "Select an active warehouse belonging to this shop."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        warehouse = assert_warehouse_access(request.user, warehouse_id, request, active_only=True)
        if warehouse.shop_id != shop.pk or not Warehouse.objects.filter(
            pk=warehouse_id, shop_id=shop_id, is_active=True
        ).exists():
            return Response(
                {"warehouse": "Select an active warehouse belonging to this shop."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        with transaction.atomic():
            purchase = Purchase.objects.select_for_update().get(pk=purchase.pk)
            if purchase.document_status != Purchase.DocumentStatus.DRAFT:
                return Response(
                    {"detail": "This purchase was posted while you were editing. Reload the list before making changes."},
                    status=status.HTTP_400_BAD_REQUEST,
                )
            if StockMovement.objects.filter(
                reference_type__in=("Purchase", "PurchaseVoid"),
                reference_id=str(purchase.pk),
            ).exists():
                return Response(
                    {"detail": "This draft already has stock receipt history. Use Post to safely reconcile a complete receipt before editing."},
                    status=status.HTTP_400_BAD_REQUEST,
                )
            purchase.shop_id = shop_id
            purchase.supplier_id = supplier_id
            purchase.currency_id = currency_id
            purchase.warehouse_id = warehouse_id
            purchase.purchase_date = data.get("purchase_date", purchase.purchase_date)
            purchase.due_date = optional_fk(data.get("due_date", purchase.due_date))
            purchase.supplier_invoice_number = data.get(
                "supplier_invoice_number", purchase.supplier_invoice_number
            )
            purchase.exchange_rate_used = optional_fk(
                data.get("exchange_rate_used", purchase.exchange_rate_used)
            )
            purchase.amount = data.get("amount", purchase.amount)
            purchase.notes = data.get("notes", purchase.notes)
            purchase.save()
            purchase.purchase_items.all().delete()
            for item in items_data:
                PurchaseItem.objects.create(
                    purchase=purchase, product_id=item.get("product"),
                    unit_id=item_products[str(item.get("product"))].default_unit_id, quantity=item.get("quantity"),
                    buy_price=item.get("buy_price"),
                    wholesale_price=item.get("wholesale_price") or None,
                    retail_price=item.get("retail_price") or None,
                    sale_price=item.get("retail_price") or item.get("sale_price"),
                    expire_date=optional_fk(item.get("expire_date")),
                    batch_number=item.get("batch_number", ""),
                    manufacture_date=optional_fk(item.get("manufacture_date")),
                    created_by=request.user,
                )
        return Response(PurchaseSerializer(purchase, context={"request": request}).data)

    @action(detail=True, methods=["post"])
    def post(self, request, pk=None):
        purchase = self.get_object()
        try:
            purchase = post_purchase(purchase.pk, user=request.user)
        except DjangoValidationError as exc:
            raise ValidationError(exc.message_dict if hasattr(exc, "message_dict") else exc.messages)
        return Response(PurchaseSerializer(purchase, context={"request": request}).data)

    @action(detail=True, methods=["post"])
    def void(self, request, pk=None):
        purchase = self.get_object()
        try:
            purchase = void_purchase(
                purchase.pk, user=request.user, reason=request.data.get("reason", "")
            )
        except DjangoValidationError as exc:
            raise ValidationError(exc.message_dict if hasattr(exc, "message_dict") else exc.messages)
        return Response(PurchaseSerializer(purchase, context={"request": request}).data)

    def _get_auto_payment(self, purchase, old_received_amount):
        auto_payment = (
            purchase.payments.filter(notes__startswith="Automatic purchase payment")
            .order_by("-id")
            .first()
        )
        if auto_payment:
            return auto_payment

        if old_received_amount and old_received_amount > 0:
            return (
                purchase.payments.filter(amount=old_received_amount)
                .order_by("-id")
                .first()
            )
        return None

    def _save_purchase_payment(self, purchase, amount, data, user, instance=None):
        account_id = optional_fk(data.get("account"))
        payment_currency_id = optional_fk(data.get("currency")) or purchase.currency_id
        exchange_rate = data.get("exchange_rate")
        if not account_id:
            return Response(
                {"account": "Account is required when received amount is greater than 0."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        payment_data = {
            "purchase": purchase.id,
            "account": account_id,
            "currency": payment_currency_id,
            "amount": amount,
            "exchange_rate": exchange_rate,
            "payment_date": data.get("payment_date") or timezone.now(),
            "notes": data.get("payment_notes") or "Automatic purchase payment from purchase form",
        }
        serializer = PurchasePaymentSerializer(
            instance=instance,
            data=payment_data,
            partial=bool(instance),
        )
        if not serializer.is_valid():
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
        serializer.save(created_by=user)
        return None

    # --------------------------------------------------------------------- stock helpers
    @staticmethod
    def _to_decimal(value, default="0"):
        try:
            if value is None or value == "":
                return Decimal(default)
            return Decimal(str(value))
        except Exception:
            return Decimal(default)

    def _weighted_avg(self, old_price, old_qty, new_price, new_qty):
        old_q = self._to_decimal(old_qty)
        new_q = self._to_decimal(new_qty)
        total_q = old_q + new_q
        if total_q <= 0:
            return self._to_decimal(old_price)
        old_p = self._to_decimal(old_price)
        new_p = self._to_decimal(new_price)
        return ((old_p * old_q) + (new_p * new_q)) / total_q

    def _sync_stock_price_payload(self, stock):
        if not stock or not stock.purchase_item:
            return

        raw_comments = {}
        if stock.comments:
            try:
                parsed = json.loads(stock.comments)
                if isinstance(parsed, dict):
                    raw_comments = parsed
            except Exception:
                raw_comments = {}

        currency = getattr(getattr(stock, "currency", None), "title", "")
        currency = str(currency or "").strip().upper()
        suffix = "afg" if currency in ("AFN", "AFG") else "usd" if currency == "USD" else ""
        if not suffix:
            return

        price_by_currency = raw_comments.get("price_by_currency", {}) or {}
        price_by_currency[f"buy_price_{suffix}"] = str(stock.average_buy_price or 0)
        price_by_currency[f"wholesale_price_{suffix}"] = (
            str(stock.average_wholesale_price)
            if getattr(stock, "average_wholesale_price", None) is not None
            else str(stock.purchase_item.wholesale_price)
            if stock.purchase_item.wholesale_price is not None
            else None
        )
        price_by_currency[f"retail_price_{suffix}"] = (
            str(stock.average_retail_price)
            if getattr(stock, "average_retail_price", None) is not None
            else str(stock.purchase_item.retail_price)
            if stock.purchase_item.retail_price is not None
            else str(stock.purchase_item.sale_price)
            if stock.purchase_item.sale_price is not None
            else None
        )
        raw_comments["price_by_currency"] = price_by_currency
        stock.comments = json.dumps(raw_comments)

    def _purchase_item_unit_price(self, purchase_item, field):
        value = getattr(purchase_item, field, None)
        if field == "retail_price" and value is None:
            value = getattr(purchase_item, "sale_price", None)
        return self._to_decimal(value or 0)

    def _weighted_price_for_delta(self, current_price, current_qty, incoming_price, incoming_qty):
        current_qty = self._to_decimal(current_qty)
        incoming_qty = self._to_decimal(incoming_qty)
        if incoming_qty <= 0:
            return self._to_decimal(current_price or incoming_price or 0)
        total_qty = current_qty + incoming_qty
        if total_qty <= 0:
            return self._to_decimal(incoming_price or current_price or 0)
        return (
            (self._to_decimal(current_price) * current_qty)
            + (self._to_decimal(incoming_price) * incoming_qty)
        ) / total_qty

    def _sync_purchase_item_stock_prices(self, shop, purchase_item):
        if not purchase_item or not purchase_item.product_id or not purchase_item.pk:
            return None
        stock_queryset = Stock.objects.select_for_update().filter(
            shop=shop,
            purchase_item__product=purchase_item.product,
            is_deleted=False,
            purchase_item__is_deleted=False,
            purchase_item__purchase__is_deleted=False,
        )
        currency_id = purchase_item.currency_id or purchase_item.purchase.currency_id
        if currency_id:
            stock_queryset = stock_queryset.filter(
                models.Q(purchase_item__currency_id=currency_id)
                | models.Q(
                    purchase_item__currency__isnull=True,
                    purchase_item__purchase__currency_id=currency_id,
                )
            )
        stock = stock_queryset.order_by("-quantity", "-id").first()
        if not stock:
            return None
        stock.purchase_item = purchase_item
        stock.average_buy_price = self._purchase_item_unit_price(purchase_item, "buy_price")
        stock.average_wholesale_price = self._purchase_item_unit_price(purchase_item, "wholesale_price")
        stock.average_retail_price = self._purchase_item_unit_price(purchase_item, "retail_price")
        stock.sale_price = purchase_item.sale_price or purchase_item.retail_price or stock.sale_price
        stock.wholesale_price = purchase_item.wholesale_price
        stock.retail_price = purchase_item.retail_price or purchase_item.sale_price
        self._sync_stock_price_payload(stock)
        stock.save()
        return stock

    @staticmethod
    def _purchase_item_must_not_move_stock(purchase_item):
        """Transfer purchases are payment records. Qty already moved on accept."""
        purchase = getattr(purchase_item, "purchase", None)
        if purchase is None:
            return False
        checker = getattr(purchase, "_is_stock_transfer_purchase", None)
        return bool(checker()) if callable(checker) else False

    def _apply_purchase_item_stock_delta(self, shop, purchase_item, quantity_delta, created_by=None):
        if not purchase_item or not purchase_item.product_id:
            return None
        if self._purchase_item_must_not_move_stock(purchase_item):
            return None
        quantity_delta = int(quantity_delta or 0)
        # Unsaved PurchaseItem snapshots (used when product changes on update) can
        # drive quantity math via product FK, but must never be assigned to Stock.
        purchase_item_is_saved = bool(purchase_item.pk)
        stock_queryset = Stock.objects.select_for_update().filter(
            shop=shop,
            purchase_item__product=purchase_item.product,
            is_deleted=False,
            purchase_item__is_deleted=False,
            purchase_item__purchase__is_deleted=False,
        )
        currency_id = (
            purchase_item.currency_id
            or getattr(getattr(purchase_item, "purchase", None), "currency_id", None)
        )
        if currency_id:
            stock_queryset = stock_queryset.filter(
                models.Q(purchase_item__currency_id=currency_id)
                | models.Q(
                    purchase_item__currency__isnull=True,
                    purchase_item__purchase__currency_id=currency_id,
                )
            )
        stocks = list(stock_queryset.order_by("-quantity", "-id"))
        stock = stocks[0] if stocks else None
        for duplicate in stocks[1:]:
            quantity_delta += int(duplicate.quantity or 0)
            duplicate.delete()

        if not stock:
            if quantity_delta <= 0 or not purchase_item_is_saved:
                return None
            stock = Stock.objects.create(
                shop=shop,
                purchase_item=purchase_item,
                quantity=0,
                average_buy_price=0,
                created_by=created_by or getattr(purchase_item, "created_by", None),
            )

        current_qty = int(stock.quantity or 0)
        next_qty = max(current_qty + quantity_delta, 0)
        if quantity_delta > 0:
            stock.average_buy_price = self._weighted_price_for_delta(
                stock.average_buy_price,
                current_qty,
                self._purchase_item_unit_price(purchase_item, "buy_price"),
                quantity_delta,
            )
            stock.average_wholesale_price = self._weighted_price_for_delta(
                stock.average_wholesale_price,
                current_qty,
                self._purchase_item_unit_price(purchase_item, "wholesale_price"),
                quantity_delta,
            )
            stock.average_retail_price = self._weighted_price_for_delta(
                stock.average_retail_price,
                current_qty,
                self._purchase_item_unit_price(purchase_item, "retail_price"),
                quantity_delta,
            )

        stock.quantity = next_qty
        if purchase_item_is_saved:
            stock.purchase_item = purchase_item
            stock.sale_price = purchase_item.sale_price or purchase_item.retail_price or stock.sale_price
            stock.wholesale_price = purchase_item.wholesale_price
            stock.retail_price = purchase_item.retail_price or purchase_item.sale_price
            self._sync_stock_price_payload(stock)
        stock.save()
        return stock

    def _recalculate_stock_for_product(self, shop, product, created_by=None):
        stock_queryset = (
            Stock.objects.select_for_update()
            .filter(
                shop=shop,
                purchase_item__product=product,
                is_deleted=False,
                purchase_item__is_deleted=False,
                purchase_item__purchase__is_deleted=False,
            )
            .order_by("-id")
        )

        stocks = list(stock_queryset)
        purchase_items = [
            item
            for item in PurchaseItem.objects.select_related("purchase", "product", "unit")
            .filter(
                purchase__shop=shop,
                product=product,
                is_deleted=False,
                purchase__is_deleted=False,
            )
            .order_by("id")
            if not self._purchase_item_must_not_move_stock(item)
        ]
        total_quantity = sum(int(item.quantity or 0) for item in purchase_items)
        if total_quantity <= 0:
            for stock_row in stocks:
                stock_row.delete()
            return None

        total_cost = sum(
            self._to_decimal(item.quantity) * self._to_decimal(item.buy_price)
            for item in purchase_items
        )
        total_wholesale = sum(
            self._to_decimal(item.quantity) * self._to_decimal(item.wholesale_price)
            for item in purchase_items
            if item.wholesale_price is not None
        )
        wholesale_quantity = sum(
            int(item.quantity or 0) for item in purchase_items if item.wholesale_price is not None
        )
        total_retail = sum(
            self._to_decimal(item.quantity) * self._to_decimal(item.retail_price or item.sale_price)
            for item in purchase_items
            if (item.retail_price is not None or item.sale_price is not None)
        )
        retail_quantity = sum(
            int(item.quantity or 0)
            for item in purchase_items
            if (item.retail_price is not None or item.sale_price is not None)
        )
        if not purchase_items or total_quantity <= 0:
            for stock_row in stocks:
                stock_row.delete()
            return None

        average_buy_price = total_cost / self._to_decimal(total_quantity)
        average_wholesale_price = (
            total_wholesale / self._to_decimal(wholesale_quantity)
            if wholesale_quantity > 0
            else None
        )
        average_retail_price = (
            total_retail / self._to_decimal(retail_quantity)
            if retail_quantity > 0
            else None
        )
        latest_item = purchase_items[-1]

        if not stocks:
            stock = Stock.objects.create(
                shop=shop,
                purchase_item=latest_item,
                quantity=total_quantity,
                average_buy_price=average_buy_price,
                average_wholesale_price=average_wholesale_price,
                average_retail_price=average_retail_price,
                sale_price=latest_item.sale_price or 0,
                wholesale_price=latest_item.wholesale_price,
                retail_price=latest_item.retail_price,
                created_by=latest_item.created_by,
            )
            self._sync_stock_price_payload(stock)
            stock.save(update_fields=["comments"])
            return stock

        stock = stocks[0]
        for duplicate in stocks[1:]:
            duplicate.delete()

        stock.quantity = total_quantity
        stock.average_buy_price = average_buy_price
        stock.average_wholesale_price = average_wholesale_price
        stock.average_retail_price = average_retail_price
        stock.purchase_item = latest_item
        if latest_item.sale_price is not None:
            stock.sale_price = latest_item.sale_price
        if average_wholesale_price is not None:
            stock.wholesale_price = average_wholesale_price
        elif latest_item.wholesale_price is not None:
            stock.wholesale_price = latest_item.wholesale_price
        if average_retail_price is not None:
            stock.retail_price = average_retail_price
        elif latest_item.retail_price is not None:
            stock.retail_price = latest_item.retail_price
        self._sync_stock_price_payload(stock)
        stock.save()
        return stock
