from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import SearchFilter
from rest_framework.permissions import IsAuthenticated
from account.permissions import (
    HasTabPermission, HasActionPermission,
    user_can_see_all_shop_data,
    get_user_data_filter,
    get_user_shop_filter,
)
from shop_management.views.data.base import DataRootViewSet
from shop_management.views.data.pagination import CustomPagination
from shop_management.serializers.data.sale import (
    SaleSerializer,
    SaleItemSerializer,
    Sale,
    SaleItem,
)
from shop_management.models.data.stock import Stock
from shop_management.models.data.stock_transfer import StockTransfer
from django.db import transaction
from django.db.models import Q, Sum
from rest_framework import status
from rest_framework.exceptions import ValidationError as DRFValidationError, PermissionDenied
from rest_framework.response import Response
from rest_framework.decorators import action
from django.core.exceptions import ValidationError as DjangoValidationError
from django.utils import timezone
from django.utils.dateparse import parse_date, parse_datetime


from shop_management.models.data.customer_balance_payment import CustomerBalancePayment
from shop_management.models.data.customer import Customer
from shop_management.models.data.account_transaction import AccountTransaction
from shop_management.models.data.shop_account import Account
from shop_management.models.data.shop import Shop
from shop_management.models.data.utils import Currency
from shop_management.models.data.organization import Warehouse
from shop_management.models.data.product import Product
from shop_management.models.data.inventory import StockMovement
from shop_management.models.data.sale import SaleItemBatchAllocation
from shop_management.services.sales import post_sale, void_sale, _calculate_totals
from shop_management.models.data.customer_payment import CustomerPayment, CustomerPaymentAllocation
from shop_management.services.customer_payments import post_customer_payment
from shop_management.serializers.data.customer_balance_payment import (
    CustomerBalancePaymentSerializer,
)


class SaleViewSet(DataRootViewSet):
    serializer_class = SaleSerializer
    permission_classes = [IsAuthenticated, HasTabPermission, HasActionPermission]
    action_permission_map = {"create": "sales.create_sale", "post": "sales.post_sale", "void": "sales.void_sale"}
    required_tab = "sales"
    filter_backends = [DjangoFilterBackend, SearchFilter]
    filterset_fields = ["shop", "customer", "status"]
    search_fields = [
        "sale_number",
        "shop__name",
        "customer__name",
        "sale_items__product__name",
        "sale_items__product__code",
        "general_customer_name",
    ]

    @staticmethod
    def _is_stock_transfer_sale(sale):
        prefix = "Stock transfer sale for "
        notes = str(getattr(sale, "notes", "") or "")
        if not notes.startswith(prefix):
            return False
        transfer_id = notes[len(prefix):].strip()
        return bool(
            transfer_id
            and StockTransfer.objects.filter(
                transfer_id=transfer_id,
                supplier_branch_id=sale.shop_id,
            ).exists()
        )

    @staticmethod
    def _returned_quantity(sale_item):
        return int(
            sale_item.return_items.aggregate(total=Sum("return_quantity"))["total"]
            or 0
        )
    pagination_class = CustomPagination

    # def get_queryset(self):
    #     return Sale.objects.filter(shop__owner=self.request.user).order_by('-id')

    def get_queryset(self):
        if user_can_see_all_shop_data(self.request.user, self.required_tab):
            queryset = Sale.objects.all()
        else:
            queryset = Sale.objects.filter(get_user_data_filter(self.request.user, "shop")).distinct()

        # Explicit branch scope for non-superusers (login selected branch / header).
        selected_shop_id = self.request.headers.get("X-Selected-Shop") or self.request.META.get(
            "HTTP_X_SELECTED_SHOP"
        )
        if selected_shop_id and not user_can_see_all_shop_data(self.request.user, self.required_tab):
            queryset = queryset.filter(shop_id=selected_shop_id)

        if self.request.query_params.get("return_eligible") == "true":
            queryset = queryset.filter(
                Q(uses_inventory_engine=False)
                | Q(document_status=Sale.DocumentStatus.POSTED)
            )

        start_date = self.request.query_params.get("start_date")
        end_date = self.request.query_params.get("end_date")

        if start_date:
            start_date = parse_date(start_date)
            if start_date:
                queryset = queryset.filter(sale_date__gte=start_date)

        if end_date:
            end_date = parse_date(end_date)
            if end_date:
                queryset = queryset.filter(sale_date__lte=end_date)

        return queryset.select_related('shop', 'customer', 'currency', 'warehouse').prefetch_related('payments__currency', 'sale_items__product', 'sale_items__unit', 'sale_items__warehouse', 'sale_items__batch_allocations__batch').distinct().order_by("-id")

    def create(self, request, *args, **kwargs):
        data = request.data
        if data.get("warehouse"):
            return self._create_engine_draft(request)
        shop_id = data.get("shop") or request.headers.get("X-Selected-Shop")
        customer_id = data.get("customer")
        is_general_customer = str(customer_id) == "general-customer"
        general_customer_name = (data.get("general_customer_name") or "").strip()
        general_customer_phone = (data.get("general_customer_phone") or "").strip()

        shop = self._get_accessible_shop(request, shop_id)
        if isinstance(shop, Response):
            return shop
        shop_id = shop.id

        currency = self._get_shop_currency(data.get("currency"), shop_id)
        if isinstance(currency, Response):
            return currency

        if customer_id and not is_general_customer:
            customer = Customer.objects.filter(pk=customer_id, shop_id=shop_id).first()
            if not customer:
                return Response(
                    {"customer": "Selected customer must belong to the selected branch."},
                    status=status.HTTP_400_BAD_REQUEST,
                )

        if is_general_customer:
            # Persist as a real customer so it can be selected later in Sales Payments.
            if general_customer_name:
                customer_obj, _ = Customer.objects.get_or_create(
                    shop_id=shop_id,
                    name=general_customer_name,
                    defaults={
                        "contact": general_customer_phone or None,
                        "is_general_customer": True,
                        "created_by": request.user,
                    },
                )
                if general_customer_phone and customer_obj.contact != general_customer_phone:
                    customer_obj.contact = general_customer_phone
                    customer_obj.save(update_fields=["contact"])
                customer_id = customer_obj.id
            else:
                customer_id = None
        discount_type = data.get("discount_type", Sale.DiscountType.FIXED)
        discount_value = data.get("discount_value", data.get("amount", 0))
        sale_date = (
            data.get("sale_date") or timezone.now().date()
        )  # sale_date is a date
        try:
            discount = Decimal(discount_value)
        except (InvalidOperation, TypeError):
            discount = Decimal(0)
        # Parse received amount
        received_amount_value = data.get("received_amount", 0)
        try:
            received_amount = Decimal(received_amount_value)
        except (InvalidOperation, TypeError):
            received_amount = Decimal(0)
        items_data = data.get("items", [])

        def parse_decimal(value, default=Decimal(0)):
            try:
                if value in (None, ""):
                    return default
                return Decimal(str(value))
            except (InvalidOperation, TypeError, ValueError):
                return default

        def positive_decimal_or_default(value, default=Decimal(0)):
            parsed = parse_decimal(value, default)
            return default if parsed <= 0 else parsed

        def get_stock_for_item(item_data, shop_id):
            purchase_item_id = item_data.get("purchase_item")
            if purchase_item_id:
                stock = Stock.objects.filter(
                    shop_id=shop_id,
                    purchase_item_id=purchase_item_id,
                    is_deleted=False,
                    purchase_item__is_deleted=False,
                    purchase_item__purchase__is_deleted=False,
                ).first()
                if stock:
                    return stock
            return Stock.objects.filter(
                shop_id=shop_id,
                purchase_item__product_id=item_data.get("product"),
                is_deleted=False,
                purchase_item__is_deleted=False,
                purchase_item__purchase__is_deleted=False,
            ).first()

        def convert_stock_amount(amount, stock, target_currency_id):
            value = parse_decimal(amount)
            source_currency_id = (
                stock.purchase_item.purchase.currency_id
                if stock and stock.purchase_item and stock.purchase_item.purchase
                else target_currency_id
            )
            if not source_currency_id or not target_currency_id or source_currency_id == int(target_currency_id):
                return value
            rate = AccountTransaction.resolve_exchange_rate(
                stock.shop,
                source_currency_id,
                int(target_currency_id),
            )
            if rate and rate > 0:
                return (value * rate).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
            return value

        with transaction.atomic():
            try:
                sale = Sale.objects.create(
                    shop_id=shop_id,
                    customer_id=customer_id,
                    general_customer_name=general_customer_name or None,
                    general_customer_phone=general_customer_phone or None,
                    discount_type=discount_type,
                    discount_value=discount,
                    amount=discount,
                    sale_date=sale_date,
                    currency=currency,
                    received_amount=received_amount,
                    notes=data.get("notes") or None,
                    created_by=request.user,
                )
            except Exception as e:
                return Response({"error": str(e)}, status=400)

            for item in items_data:
                try:
                    # If sale_price not provided, get it from stock
                    sale_price_input = item.get("sale_price")
                    stock = get_stock_for_item(item, shop_id)
                    if not stock:
                        transaction.set_rollback(True)
                        return Response(
                            {
                                "stock": (
                                    "Selected product is not available in the "
                                    "selected branch stock."
                                )
                            },
                            status=status.HTTP_400_BAD_REQUEST,
                        )
                    
                    if not sale_price_input or parse_decimal(sale_price_input) <= 0:
                        sale_price_val = convert_stock_amount(
                            stock.retail_price or stock.sale_price if stock else 0,
                            stock,
                            currency.id,
                        )
                    else:
                        sale_price_val = sale_price_input
                    buy_price_val = positive_decimal_or_default(
                        item.get("buy_price"),
                        convert_stock_amount(
                            getattr(stock, "average_buy_price", None)
                            if stock and getattr(stock, "average_buy_price", None) is not None
                            else (
                                getattr(stock.purchase_item, "buy_price", Decimal(0))
                                if stock and stock.purchase_item
                                else Decimal(0)
                            ),
                            stock,
                            currency.id,
                        ),
                    )

                    sale_item = SaleItem.objects.create(
                        sale=sale,
                        product_id=item.get("product"),
                        purchase_item=stock.purchase_item if stock else None,
                        quantity=item.get("quantity"),
                        unit_id=item.get("unit"),
                        buy_price=buy_price_val,
                        sale_price=sale_price_val,
                        created_by=request.user,
                    )
                    self._update_stock_on_create(sale, sale_item)
                except Exception as e:
                    transaction.set_rollback(True)
                    return Response({"error": str(e)}, status=400)

            subtotal = sum((Decimal(str(item.quantity)) * item.sale_price for item in sale.sale_items.all()), Decimal("0"))
            if discount_type == Sale.DiscountType.PERCENTAGE:
                disc_amount = (subtotal * discount / Decimal("100")).quantize(Decimal("0.01"))
            else:
                disc_amount = discount.quantize(Decimal("0.01"))
            sale.amount = disc_amount
            sale.discount_amount = disc_amount
            sale.calculated_subtotal = subtotal
            sale.calculated_total = max(subtotal - disc_amount, Decimal("0"))
            sale.save(update_fields=["amount", "discount_amount", "calculated_subtotal", "calculated_total"])

            if received_amount > 0 and customer_id and currency:
                payment_response = self._save_sale_payment(
                    sale=sale,
                    amount=received_amount,
                    data=data,
                    user=request.user,
                )
                if payment_response is not None:
                    transaction.set_rollback(True)
                    return payment_response

            return Response({"message": "Sale created successfully"}, status=201)

    def update(self, request, *args, **kwargs):
        sale = self.get_object()

        if sale.uses_inventory_engine:
            return self._update_engine_draft(request, sale)

        if self._is_stock_transfer_sale(sale):
            return Response(
                {"detail": "Manage this record from Stock Transfer, not Sales."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        data = request.data
        old_received_amount = sale.received_amount or Decimal(0)
        shop_id = data.get("shop", sale.shop_id) or request.headers.get("X-Selected-Shop")
        customer_id = data.get("customer", sale.customer_id)
        is_general_customer = str(customer_id) == "general-customer"
        general_customer_name = (data.get("general_customer_name") or "").strip()
        general_customer_phone = (data.get("general_customer_phone") or "").strip()
        if is_general_customer:
            if general_customer_name:
                customer_obj, _ = Customer.objects.get_or_create(
                    shop_id=shop_id,
                    name=general_customer_name,
                    defaults={
                        "contact": general_customer_phone or None,
                        "is_general_customer": True,
                        "created_by": request.user,
                    },
                )
                if general_customer_phone and customer_obj.contact != general_customer_phone:
                    customer_obj.contact = general_customer_phone
                    customer_obj.save(update_fields=["contact"])
                customer_id = customer_obj.id
            else:
                customer_id = None
        discount_type = data.get("discount_type", sale.discount_type)
        discount_value = data.get("discount_value", data.get("amount", sale.discount_value or sale.amount))
        sale_date = data.get("sale_date", sale.sale_date)
        try:
            discount = Decimal(discount_value)
        except (InvalidOperation, TypeError):
            discount = Decimal(0)
        received_amount_value = data.get("received_amount", sale.received_amount or 0)
        try:
            received_amount = Decimal(received_amount_value)
        except (InvalidOperation, TypeError):
            received_amount = Decimal(0)
        items_data = data.get("items", [])

        shop = self._get_accessible_shop(request, shop_id)
        if isinstance(shop, Response):
            return shop
        shop_id = shop.id

        currency = self._get_shop_currency(data.get("currency", sale.currency_id), shop_id)
        if isinstance(currency, Response):
            return currency

        if customer_id and not is_general_customer:
            customer = Customer.objects.filter(pk=customer_id, shop_id=shop_id).first()
            if not customer:
                return Response(
                    {"customer": "Selected customer must belong to the selected branch."},
                    status=status.HTTP_400_BAD_REQUEST,
                )

        def parse_decimal(value, default=Decimal(0)):
            try:
                if value in (None, ""):
                    return default
                return Decimal(str(value))
            except (InvalidOperation, TypeError, ValueError):
                return default

        def positive_decimal_or_default(value, default=Decimal(0)):
            parsed = parse_decimal(value, default)
            return default if parsed <= 0 else parsed

        def get_stock_for_item(item_data, shop_id):
            purchase_item_id = item_data.get("purchase_item")
            if purchase_item_id:
                stock = Stock.objects.filter(
                    shop_id=shop_id,
                    purchase_item_id=purchase_item_id,
                    is_deleted=False,
                    purchase_item__is_deleted=False,
                    purchase_item__purchase__is_deleted=False,
                ).first()
                if stock:
                    return stock
            return Stock.objects.filter(
                shop_id=shop_id,
                purchase_item__product_id=item_data.get("product"),
                is_deleted=False,
                purchase_item__is_deleted=False,
                purchase_item__purchase__is_deleted=False,
            ).first()

        def convert_stock_amount(amount, stock, target_currency_id):
            value = parse_decimal(amount)
            source_currency_id = (
                stock.purchase_item.purchase.currency_id
                if stock and stock.purchase_item and stock.purchase_item.purchase
                else target_currency_id
            )
            if not source_currency_id or not target_currency_id or source_currency_id == int(target_currency_id):
                return value
            rate = AccountTransaction.resolve_exchange_rate(
                stock.shop,
                source_currency_id,
                int(target_currency_id),
            )
            if rate and rate > 0:
                return AccountTransaction.compute_converted_amount(value, rate)
            return value

        with transaction.atomic():
            sale.shop_id = shop_id
            sale.customer_id = customer_id
            sale.general_customer_name = general_customer_name or None
            sale.general_customer_phone = general_customer_phone or None
            sale.discount_type = discount_type
            sale.discount_value = discount
            sale.amount = discount
            sale.currency = currency
            sale.sale_date = sale_date
            sale.received_amount = received_amount
            if "notes" in data:
                sale.notes = data.get("notes") or None
            sale.save()

            existing_items = {str(item.id): item for item in sale.sale_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:
                    sale_item = existing_items[str(item_id)]
                    old_quantity = int(sale_item.quantity)
                    returned_quantity = self._returned_quantity(sale_item)
                    old_product_id = sale_item.product_id
                    old_purchase_item_id = sale_item.purchase_item_id
                    new_quantity = int(item_data.get("quantity", sale_item.quantity))
                    if new_quantity < returned_quantity:
                        raise DRFValidationError(
                            {
                                "quantity": (
                                    "Sale quantity cannot be less than the quantity "
                                    "already returned."
                                )
                            }
                        )
                    sale_item.product_id = item_data.get(
                        "product", sale_item.product_id
                    )
                    sale_item.unit_id = item_data.get("unit", sale_item.unit_id)
                    sale_price_input = item_data.get("sale_price")
                    stock = get_stock_for_item(item_data, shop_id)
                    stock_identity_changed = (
                        sale_item.product_id != old_product_id
                        or (
                            stock
                            and old_purchase_item_id
                            and stock.purchase_item_id != old_purchase_item_id
                        )
                    )
                    if stock_identity_changed and returned_quantity > 0:
                        raise DRFValidationError(
                            {
                                "product": (
                                    "A product with sale returns cannot be changed. "
                                    "Delete the return first."
                                )
                            }
                        )
                    if stock_identity_changed and not stock:
                        raise DRFValidationError(
                            {"stock": "Selected product is not available in this branch."}
                        )
                    if stock_identity_changed:
                        sale_item.product_id = old_product_id
                        sale_item.purchase_item_id = old_purchase_item_id
                        sale_item.quantity = old_quantity
                        self._update_stock_on_delete(sale, sale_item)
                        sale_item.product_id = item_data.get("product", old_product_id)
                        sale_item.quantity = new_quantity
                    
                    if not sale_price_input or parse_decimal(sale_price_input) <= 0:
                        sale_price_val = (
                            convert_stock_amount(
                                stock.retail_price or stock.sale_price if stock else sale_item.sale_price,
                                stock,
                                data.get("currency", sale.currency_id),
                            )
                        )
                    else:
                        sale_price_val = sale_price_input
                    
                    if stock:
                        sale_item.purchase_item = stock.purchase_item
                    
                    sale_item.sale_price = sale_price_val
                    sale_item.buy_price = positive_decimal_or_default(
                        item_data.get("buy_price"),
                        sale_item.buy_price or convert_stock_amount(
                            getattr(stock.purchase_item, "buy_price", Decimal(0)) if stock and stock.purchase_item else Decimal(0),
                            stock,
                            data.get("currency", sale.currency_id),
                        ),
                    )
                    sale_item.quantity = new_quantity
                    sale_item.save()
                    new_items_ids.append(str(item_id))
                    quantity_diff = (
                        new_quantity
                        if stock_identity_changed
                        else new_quantity - old_quantity
                    )
                    self._update_stock_on_update(sale, sale_item, quantity_diff)
                else:
                    new_quantity = int(item_data.get("quantity"))
                    sale_price_input = item_data.get("sale_price")
                    stock = get_stock_for_item(item_data, shop_id)
                    if not stock:
                        transaction.set_rollback(True)
                        return Response(
                            {
                                "stock": (
                                    "Selected product is not available in the "
                                    "selected branch stock."
                                )
                            },
                            status=status.HTTP_400_BAD_REQUEST,
                        )
                    
                    if not sale_price_input or parse_decimal(sale_price_input) <= 0:
                        sale_price_val = convert_stock_amount(
                            stock.retail_price or stock.sale_price if stock else 0,
                            stock,
                            data.get("currency", sale.currency_id),
                        )
                    else:
                        sale_price_val = sale_price_input
                    buy_price_val = positive_decimal_or_default(
                        item_data.get("buy_price"),
                        convert_stock_amount(
                            getattr(stock, "average_buy_price", None)
                            if stock and getattr(stock, "average_buy_price", None) is not None
                            else (
                                getattr(stock.purchase_item, "buy_price", Decimal(0))
                                if stock and stock.purchase_item
                                else Decimal(0)
                            ),
                            stock,
                            data.get("currency", sale.currency_id),
                        ),
                    )
                        
                    sale_item = SaleItem.objects.create(
                        sale=sale,
                        product_id=item_data.get("product"),
                        purchase_item=stock.purchase_item if stock else None,
                        quantity=new_quantity,
                        unit_id=item_data.get("unit"),
                        buy_price=buy_price_val,
                        sale_price=sale_price_val,
                        created_by=request.user,
                    )
                    self._update_stock_on_create(sale, sale_item)
                    new_items_ids.append(str(sale_item.id))

            for item_id, sale_item in existing_items.items():
                if item_id not in new_items_ids:
                    if self._returned_quantity(sale_item) > 0:
                        raise DRFValidationError(
                            {
                                "items": (
                                    "A sale item with returns cannot be removed. "
                                    "Delete the return first."
                                )
                            }
                        )
                    self._update_stock_on_delete(sale, sale_item)
                    sale_item.delete()

            subtotal = sum((Decimal(str(item.quantity)) * item.sale_price for item in sale.sale_items.all()), Decimal("0"))
            if discount_type == Sale.DiscountType.PERCENTAGE:
                disc_amount = (subtotal * discount / Decimal("100")).quantize(Decimal("0.01"))
            else:
                disc_amount = discount.quantize(Decimal("0.01"))
            sale.amount = disc_amount
            sale.discount_amount = disc_amount
            sale.calculated_subtotal = subtotal
            sale.calculated_total = max(subtotal - disc_amount, Decimal("0"))
            sale.save(update_fields=["amount", "discount_amount", "calculated_subtotal", "calculated_total"])

            if received_amount > 0 and sale.customer_id and sale.currency_id:
                payment_response = self._save_sale_payment(
                    sale=sale,
                    amount=received_amount,
                    data=data,
                    user=request.user,
                    instance=self._get_auto_payment(sale, old_received_amount),
                )
                if payment_response is not None:
                    transaction.set_rollback(True)
                    return payment_response
            else:
                self._delete_auto_payment(self._get_auto_payment(sale, old_received_amount))

            return Response({"message": "Sale updated successfully"}, status=200)

    def destroy(self, request, *args, **kwargs):
        sale = self.get_object()

        if sale.uses_inventory_engine:
            if sale.document_status != Sale.DocumentStatus.DRAFT:
                return Response({"detail": "Posted or cancelled sales cannot be deleted."}, status=status.HTTP_400_BAD_REQUEST)
            if StockMovement.objects.filter(
                reference_type__in=("Sale", "SaleVoid"),
                reference_id=str(sale.pk),
            ).exists():
                return Response(
                    {
                        "detail": (
                            "This draft has stock ledger movements and cannot be "
                            "deleted. Contact an administrator to repair its stock "
                            "history first."
                        )
                    },
                    status=status.HTTP_400_BAD_REQUEST,
                )
            sale.delete()
            return Response(status=status.HTTP_204_NO_CONTENT)

        if self._is_stock_transfer_sale(sale):
            return Response(
                {"detail": "Delete this record from Stock Transfer, not Sales."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        with transaction.atomic():
            self._delete_auto_payment(self._get_auto_payment(sale, sale.received_amount or Decimal(0)))
            for sale_item in sale.sale_items.all():
                quantity_to_restore = max(
                    int(sale_item.quantity or 0) - self._returned_quantity(sale_item),
                    0,
                )
                if quantity_to_restore:
                    original_quantity = sale_item.quantity
                    sale_item.quantity = quantity_to_restore
                    try:
                        self._update_stock_on_delete(sale, sale_item)
                    finally:
                        sale_item.quantity = original_quantity
            sale.delete()
        return Response({"message": "Sale deleted successfully"}, status=204)

    def _engine_values(self, request, sale=None):
        data = request.data
        shop_id = data.get("shop") or (sale.shop_id if sale else None) or request.headers.get("X-Selected-Shop")
        shop = self._get_accessible_shop(request, shop_id)
        if isinstance(shop, Response):
            raise DRFValidationError(shop.data)
        warehouse_id = data.get("warehouse", sale.warehouse_id if sale else None)
        warehouse = Warehouse.objects.filter(pk=warehouse_id, shop=shop, is_active=True).first()
        if not warehouse:
            raise DRFValidationError({"warehouse": "Select an active warehouse belonging to this branch."})
        currency = self._get_shop_currency(data.get("currency", sale.currency_id if sale else None), shop.id)
        if isinstance(currency, Response):
            raise DRFValidationError(currency.data)
        customer_id = data.get("customer", sale.customer_id if sale else None)
        if customer_id in (None, "", "general-customer"):
            general_customer = Customer.objects.filter(shop=shop, is_general_customer=True).order_by("id").first()
            if not general_customer:
                general_customer = Customer.objects.create(
                    shop=shop,
                    name="General Customer",
                    is_general_customer=True,
                    created_by=request.user,
                )
            customer_id = general_customer.id
        if customer_id and not Customer.objects.filter(pk=customer_id, shop=shop).exists():
            raise DRFValidationError({"customer": "Customer must belong to this branch."})
        sales_type = data.get("sales_type", sale.sales_type if sale else Sale.SalesType.CASH)
        if sales_type == Sale.SalesType.CREDIT and not customer_id:
            raise DRFValidationError({"customer": "Customer is required for a credit sale."})
        try:
            rate = Decimal(str(data.get("exchange_rate_used", sale.exchange_rate_used if sale else 1)))
        except (InvalidOperation, TypeError, ValueError):
            raise DRFValidationError({"exchange_rate_used": "Enter a valid exchange rate."})
        if rate <= 0:
            raise DRFValidationError({"exchange_rate_used": "Exchange rate must be greater than zero."})
        return data, shop, warehouse, currency, customer_id, sales_type, rate

    def _replace_engine_items(self, sale, items, user):
        SaleItemBatchAllocation.objects.filter(sale_item__sale=sale).delete()
        sale.sale_items.all().delete()
        if not items:
            raise DRFValidationError({"items": "At least one sale item is required."})
        for index, row in enumerate(items):
            product = Product.objects.filter(pk=row.get("product"), shop=sale.shop).first()
            if not product:
                raise DRFValidationError({f"items[{index}]": "Product must belong to this branch."})
            try:
                quantity = int(row.get("quantity"))
                price = Decimal(str(row.get("sale_price")))
            except (TypeError, ValueError, InvalidOperation):
                raise DRFValidationError({f"items[{index}]": "Enter valid quantity and sale price."})
            if quantity <= 0 or price < 0:
                raise DRFValidationError({f"items[{index}]": "Quantity must be positive and price nonnegative."})
            warehouse_id = row.get("warehouse") or sale.warehouse_id
            item_warehouse = Warehouse.objects.filter(
                pk=warehouse_id, shop=sale.shop, is_active=True
            ).first()
            if not item_warehouse:
                raise DRFValidationError({f"items[{index}].warehouse": "Select an active warehouse belonging to this branch."})
            item = SaleItem.objects.create(
                sale=sale, product=product, warehouse=item_warehouse,
                unit_id=row.get("unit"), quantity=quantity,
                sale_price=price, buy_price=None, created_by=user,
            )
            for allocation in row.get("batch_allocations", []):
                SaleItemBatchAllocation.objects.create(
                    sale_item=item, batch_id=allocation.get("batch"),
                    quantity=allocation.get("quantity"), created_by=user,
                )

    def _create_engine_draft(self, request):
        data, shop, warehouse, currency, customer_id, sales_type, rate = self._engine_values(request)
        complete_immediately = str(data.get("complete_immediately", "")).lower() in {"1", "true", "yes"}
        try:
            received_val = Decimal(str(data.get("received_amount") or 0))
        except (ValueError, TypeError, InvalidOperation):
            received_val = Decimal("0")
        if received_val > 0 and not complete_immediately:
            raise DRFValidationError({"received_amount": "Post the draft before recording customer payment."})
        with transaction.atomic():
            sale = Sale.objects.create(
                shop=shop, warehouse=warehouse, customer_id=customer_id, currency=currency,
                sale_date=data.get("sale_date") or timezone.localdate(),
                invoice_date=data.get("invoice_date") or data.get("sale_date") or timezone.localdate(),
                due_date=data.get("due_date") or None, sales_type=sales_type,
                exchange_rate_used=rate, discount_type=data.get("discount_type", Sale.DiscountType.FIXED),
                discount_value=data.get("discount_value", data.get("amount", 0)) or 0,
                received_amount=0, notes=data.get("notes") or "",
                general_customer_name=data.get("general_customer_name") or None,
                general_customer_phone=data.get("general_customer_phone") or None,
                uses_inventory_engine=True, document_status=Sale.DocumentStatus.DRAFT,
                created_by=request.user,
            )
            self._replace_engine_items(sale, data.get("items", []), request.user)
            try:
                subtotal, discount, total = _calculate_totals(sale, sale.sale_items.all())
                sale.calculated_subtotal = subtotal
                sale.discount_amount = discount
                sale.calculated_total = total
                sale.amount = discount
                sale.save(update_fields=["calculated_subtotal", "discount_amount", "calculated_total", "amount"])
            except DjangoValidationError:
                pass
            if complete_immediately:
                try:
                    sale = self._complete_engine_sale(
                        sale,
                        request.user,
                        financial_account_id=data.get("financial_account") or data.get("account"),
                        received_amount=received_val,
                        exchange_rate_used=data.get("exchange_rate"),
                        payment_notes=data.get("payment_notes"),
                        payment_date=data.get("payment_date") or data.get("sale_date"),
                    )
                except DjangoValidationError as exc:
                    raise DRFValidationError(
                        exc.message_dict if hasattr(exc, "message_dict") else exc.messages
                    )
        return Response(self.get_serializer(sale).data, status=status.HTTP_201_CREATED)

    def _update_engine_draft(self, request, sale):
        if sale.document_status != Sale.DocumentStatus.DRAFT:
            raise DRFValidationError({"detail": "Only draft sales can be edited."})
        data, shop, warehouse, currency, customer_id, sales_type, rate = self._engine_values(request, sale)
        with transaction.atomic():
            sale.shop, sale.warehouse, sale.currency = shop, warehouse, currency
            sale.customer_id, sale.sales_type, sale.exchange_rate_used = customer_id, sales_type, rate
            sale.sale_date = data.get("sale_date", sale.sale_date)
            sale.invoice_date = data.get("invoice_date", sale.invoice_date or sale.sale_date)
            sale.due_date = data.get("due_date", sale.due_date) or None
            sale.discount_type = data.get("discount_type", sale.discount_type)
            sale.discount_value = data.get("discount_value", data.get("amount", sale.discount_value)) or 0
            sale.notes = data.get("notes", sale.notes)
            sale.save()
            self._replace_engine_items(sale, data.get("items", []), request.user)
            try:
                subtotal, discount, total = _calculate_totals(sale, sale.sale_items.all())
                sale.calculated_subtotal = subtotal
                sale.discount_amount = discount
                sale.calculated_total = total
                sale.amount = discount
                sale.save(update_fields=["calculated_subtotal", "discount_amount", "calculated_total", "amount"])
            except DjangoValidationError:
                pass
        return Response(self.get_serializer(sale).data)

    @action(detail=True, methods=["post"])
    def post(self, request, pk=None):
        sale = self.get_object()
        try:
            with transaction.atomic():
                result = self._complete_engine_sale(
                    sale,
                    request.user,
                    financial_account_id=request.data.get("financial_account") or request.data.get("account"),
                    received_amount=request.data.get("received_amount"),
                    exchange_rate_used=request.data.get("exchange_rate"),
                    payment_notes=request.data.get("payment_notes"),
                    payment_date=request.data.get("payment_date") or request.data.get("sale_date"),
                )
        except DjangoValidationError as exc:
            raise DRFValidationError(exc.message_dict if hasattr(exc, "message_dict") else exc.messages)
        return Response(self.get_serializer(result).data)

    @staticmethod
    def _snapshot_previous_balance(sale, is_general_customer):
        """Record what the customer owed (every currency) right before this sale."""
        cents = Decimal("0.01")
        if not sale.customer_id or is_general_customer or not sale.currency_id:
            sale.previous_balance = Decimal("0")
            sale.previous_balance_details = []
            sale.save(update_fields=["previous_balance", "previous_balance_details"])
            return

        rate_date = sale.invoice_date or sale.sale_date
        owed = sale.customer.get_total_owed_by_currency()
        details = []
        total_converted = Decimal("0")
        for title, amount in owed.items():
            amount = Decimal(str(amount)).quantize(cents)
            if amount <= 0:
                continue
            currency = Currency.objects.filter(shop=sale.shop, title=title).first()
            rate = None
            if currency:
                rate = AccountTransaction.resolve_exchange_rate(
                    sale.shop, currency.id, sale.currency_id, effective_date=rate_date
                ) or AccountTransaction.resolve_exchange_rate(sale.shop, currency.id, sale.currency_id)
            converted = AccountTransaction.compute_converted_amount(amount, rate) if rate else None
            if converted is not None:
                total_converted += converted
            details.append({
                "currency_id": currency.id if currency else None,
                "currency": title,
                "amount": str(amount),
                "rate": str(rate.quantize(Decimal("0.000001"))) if rate else None,
                "converted": str(converted) if converted is not None else None,
            })
        # Invoice currency first, then the others alphabetically.
        details.sort(key=lambda row: (row["currency_id"] != sale.currency_id, row["currency"]))
        sale.previous_balance = total_converted.quantize(cents)
        sale.previous_balance_details = details
        sale.save(update_fields=["previous_balance", "previous_balance_details"])

    def _complete_engine_sale(
        self,
        sale,
        user,
        financial_account_id=None,
        received_amount=None,
        exchange_rate_used=None,
        payment_notes=None,
        payment_date=None,
    ):
        is_general_customer = bool(sale.customer_id and sale.customer.is_general_customer)
        payment_account = None
        try:
            received_decimal = Decimal(str(received_amount or 0))
        except (ValueError, TypeError, InvalidOperation):
            received_decimal = Decimal("0")

        if is_general_customer:
            payment_account = Account.objects.filter(
                pk=financial_account_id,
                shop=sale.shop,
                currency=sale.currency,
                is_active=True,
                is_system_account=False,
            ).first()
            if not payment_account:
                raise DjangoValidationError({
                    "financial_account": "General Customer sales must be paid in full into an active account using the sale currency."
                })
        elif received_decimal > 0:
            if not financial_account_id:
                raise DjangoValidationError({
                    "financial_account": "Please select an active financial account to collect the received amount."
                })
            payment_account = Account.objects.filter(
                pk=financial_account_id,
                shop=sale.shop,
                is_active=True,
                is_system_account=False,
            ).first()
            if not payment_account:
                raise DjangoValidationError({
                    "financial_account": "The selected payment account was not found or is inactive."
                })

        self._snapshot_previous_balance(sale, is_general_customer)

        result = post_sale(sale.pk, user=user)

        if is_general_customer:
            remaining_payment = max(Decimal("0"), result.calculated_total - Decimal(str(result.total_paid.get(result.currency.title, 0))))
            if remaining_payment == 0:
                return result
            payment = CustomerPayment.objects.create(
                customer=sale.customer,
                shop=sale.shop,
                financial_account=payment_account,
                currency=sale.currency,
                amount=remaining_payment,
                payment_date=result.invoice_date,
                exchange_rate_used=1,
                reference=f"CASH-{result.sale_number}",
                notes=f"Automatic full payment for General Customer sale {result.sale_number}",
                created_by=user,
            )
            CustomerPaymentAllocation.objects.create(
                payment=payment,
                sale=result,
                allocated_amount=remaining_payment,
                created_by=user,
            )
            post_customer_payment(payment.pk, user=user)
        elif received_decimal > 0 and payment_account:
            try:
                rate = Decimal(str(exchange_rate_used or 1))
                if rate <= 0:
                    rate = Decimal("1")
            except (ValueError, TypeError, InvalidOperation):
                rate = Decimal("1")

            currency_title = result.currency.title
            remaining_due = max(
                Decimal("0"),
                Decimal(str(result.total_remained_amount.get(currency_title, 0))),
            )
            allocated = min(received_decimal, remaining_due)
            pay_date = payment_date or result.invoice_date
            if isinstance(pay_date, str) and "T" in pay_date:
                pay_date = pay_date.split("T")[0]

            payment = CustomerPayment.objects.create(
                customer=sale.customer,
                shop=sale.shop,
                financial_account=payment_account,
                currency=sale.currency,
                amount=received_decimal,
                payment_date=pay_date,
                exchange_rate_used=rate,
                reference=f"SALE-{result.sale_number}",
                notes=payment_notes or f"Direct payment collected for sale {result.sale_number}",
                created_by=user,
            )
            if allocated > 0:
                CustomerPaymentAllocation.objects.create(
                    payment=payment,
                    sale=result,
                    allocated_amount=allocated,
                    created_by=user,
                )
            post_customer_payment(payment.pk, user=user)

        return result

    @action(detail=True, methods=["post"])
    @transaction.atomic
    def revise(self, request, pk=None):
        if not request.user.is_superuser:
            raise PermissionDenied("Only a super admin can revise a completed sale.")
        original = self.get_object()
        original = Sale.objects.select_for_update().get(pk=original.pk)
        if not original.uses_inventory_engine or original.document_status != Sale.DocumentStatus.POSTED:
            raise DRFValidationError({"detail": "Only completed inventory sales can be revised."})
        if original.sale_returns.filter(uses_inventory_engine=True, document_status="POSTED").exists():
            raise DRFValidationError({"detail": "Cancel this invoice's posted returns before revising it."})
        if str(request.data.get("shop", original.shop_id)) != str(original.shop_id):
            raise DRFValidationError({"shop": "The corrected invoice must remain in its original branch."})
        payload = request.data.copy()
        payload["shop"] = original.shop_id
        payload["complete_immediately"] = True
        # Reuse a cash customer's original deposit account when fully paid by credit.
        if not payload.get("financial_account") and not payload.get("account"):
            old_payment = original.customer_payment_allocations.filter(payment__status="POSTED").select_related("payment").first()
            if old_payment:
                payload["financial_account"] = old_payment.payment.financial_account_id
        saved_data = request._full_data
        try:
            void_sale(original.pk, user=request.user, reason="Replaced by an admin correction", preserve_payments=True)
            request._full_data = payload
            response = self._create_engine_draft(request)
            replacement = Sale.objects.get(pk=response.data["id"])
            replacement.replaces_sale = original
            replacement.save(update_fields=["replaces_sale"])
            original.void_reason = f"Replaced by corrected invoice {replacement.sale_number}"
            original.save(update_fields=["void_reason"])
            return Response(self.get_serializer(replacement).data, status=status.HTTP_200_OK)
        except DjangoValidationError as exc:
            raise DRFValidationError(exc.message_dict if hasattr(exc, "message_dict") else exc.messages)
        finally:
            request._full_data = saved_data

    @action(detail=True, methods=["post"])
    def void(self, request, pk=None):
        sale = self.get_object()
        try:
            result = void_sale(sale.pk, user=request.user, reason=request.data.get("reason", ""))
        except DjangoValidationError as exc:
            raise DRFValidationError(exc.message_dict if hasattr(exc, "message_dict") else exc.messages)
        return Response(self.get_serializer(result).data)

    def _get_auto_payment(self, sale, old_received_amount):
        auto_payment = (
            CustomerBalancePayment.objects.filter(
                notes__startswith=f"Automatic sale payment for sale #{sale.id}",
            )
            .order_by("-id")
            .first()
        )
        if auto_payment:
            return auto_payment

        if old_received_amount and old_received_amount > 0 and sale.customer_id:
            return (
                CustomerBalancePayment.objects.filter(
                    customer_id=sale.customer_id,
                    currency_id=sale.currency_id,
                    amount=old_received_amount,
                )
                .order_by("-id")
                .first()
            )
        return None

    def _delete_auto_payment(self, payment):
        if not payment:
            return
        AccountTransaction.objects.filter(
            description__startswith=f"Customer balance payment #{payment.id} "
        ).delete()
        payment.delete()

    def _save_sale_payment(self, sale, amount, data, user, instance=None):
        account_id = data.get("account")
        exchange_rate_value = 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,
            )
        if not sale.customer_id:
            return Response(
                {"customer": "Customer is required when received amount is greater than 0."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if not sale.customer:
            return Response(
                {"customer": "Selected customer was not found."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if str(sale.customer.shop_id) != str(sale.shop_id):
            return Response(
                {"customer": "Selected customer must belong to the same shop as sale."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if not sale.currency:
            return Response(
                {"currency": "Currency is required when received amount is greater than 0."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if str(sale.currency.shop_id) != str(sale.shop_id):
            return Response(
                {"currency": "Selected currency must belong to the same shop as sale."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        try:
            exchange_rate = Decimal(exchange_rate_value) if exchange_rate_value else None
        except (InvalidOperation, TypeError):
            exchange_rate = None

        try:
            account_id = int(account_id)
        except (TypeError, ValueError):
            return Response(
                {"account": "Invalid account."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        try:
            account = Account.objects.select_related("currency", "shop").get(pk=account_id)
        except Account.DoesNotExist:
            return Response(
                {"account": "Invalid account."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if not account.currency:
            return Response(
                {"account": "Selected account does not have a currency."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if str(account.shop_id) != str(sale.shop_id):
            return Response(
                {"account": "Selected account must belong to the same shop as sale."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        if exchange_rate is None:
            exchange_rate = AccountTransaction.resolve_exchange_rate(
                sale.shop, sale.currency_id, account.currency_id
            )
        if exchange_rate is None:
            return Response(
                {"account": "No exchange rate found between sale currency and account currency."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        if exchange_rate <= 0:
            return Response(
                {"exchange_rate": "Exchange rate must be greater than 0."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        converted_amount = AccountTransaction.compute_converted_amount(
            amount, exchange_rate
        )
        payment_notes = (data.get("payment_notes") or "").strip()
        notes = f"Automatic sale payment for sale #{sale.id}"
        if payment_notes:
            notes = f"{notes} | {payment_notes}"
        payment_date_value = data.get("payment_date")
        payment_date = None
        if payment_date_value:
            payment_date = parse_datetime(str(payment_date_value))
            if payment_date is None:
                parsed_date = parse_date(str(payment_date_value))
                if parsed_date:
                    payment_date = timezone.make_aware(
                        timezone.datetime.combine(parsed_date, timezone.datetime.min.time()),
                        timezone.get_current_timezone(),
                    )
        if payment_date is None:
            payment_date = timezone.now()

        if instance:
            instance.customer_id = sale.customer_id
            instance.account_id = account_id
            instance.currency_id = sale.currency_id
            instance.amount = amount
            instance.exchange_rate = exchange_rate
            instance.converted_amount = converted_amount
            instance.payment_date = payment_date
            instance.notes = notes
            instance.save()
            payment = instance
        else:
            payment = CustomerBalancePayment.objects.create(
                customer_id=sale.customer_id,
                account_id=account_id,
                currency_id=sale.currency_id,
                amount=amount,
                exchange_rate=exchange_rate,
                converted_amount=converted_amount,
                payment_date=payment_date,
                notes=notes,
                created_by=user,
            )

        try:
            CustomerBalancePaymentSerializer()._sync_account_transactions(payment)
        except DRFValidationError as exc:
            return Response(exc.detail, status=status.HTTP_400_BAD_REQUEST)
        return None

    def _get_accessible_shop(self, request, shop_id):
        if not shop_id:
            return Response(
                {"shop": "Please select a branch before creating a sale."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        shop = Shop.objects.filter(get_user_shop_filter(request.user), pk=shop_id).first()
        if not shop:
            return Response(
                {"shop": "You do not have access to the selected branch."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        return shop

    def _get_shop_currency(self, currency_id, shop_id):
        if not currency_id:
            return Response(
                {"currency": "Currency is required."},
                status=status.HTTP_400_BAD_REQUEST,
            )

        currency = Currency.objects.filter(pk=currency_id, shop_id=shop_id).first()
        if not currency:
            return Response(
                {"currency": "Selected currency must belong to the selected branch."},
                status=status.HTTP_400_BAD_REQUEST,
            )
        return currency

    # ---------- Stock update helper methods for Sale ----------
    def _update_stock_on_create(self, sale, sale_item):
        if self._is_stock_transfer_sale(sale):
            return
        qty_to_reduce = int(sale_item.quantity)
        stocks = Stock.objects.select_for_update().filter(
            shop=sale.shop, 
            purchase_item__product=sale_item.product,
            is_deleted=False,
            purchase_item__is_deleted=False,
            purchase_item__purchase__is_deleted=False,
        ).order_by("id")
        if sale_item.purchase_item_id:
            selected_stock = stocks.filter(
                purchase_item_id=sale_item.purchase_item_id
            ).first()
            stocks = (
                stocks.filter(purchase_item_id=sale_item.purchase_item_id)
                if selected_stock
                else stocks
            )
        stocks = list(stocks)
        if sum(int(stock.quantity or 0) for stock in stocks) < qty_to_reduce:
            raise DRFValidationError(
                {"quantity": "Sale quantity cannot be greater than available stock."}
            )
        for stock in stocks:
            if qty_to_reduce <= 0:
                break
            available = int(stock.quantity or 0)
            if available >= qty_to_reduce:
                stock.quantity = available - qty_to_reduce
                stock.sale_price = sale_item.sale_price
                stock.save(update_fields=["quantity", "sale_price", "updated_at"])
                qty_to_reduce = 0
            else:
                stock.quantity = 0
                stock.sale_price = sale_item.sale_price
                stock.save(update_fields=["quantity", "sale_price", "updated_at"])
                qty_to_reduce -= available

    def _update_stock_on_update(self, sale, sale_item, quantity_diff):
        if quantity_diff > 0:
            original_quantity = sale_item.quantity
            sale_item.quantity = quantity_diff
            try:
                self._update_stock_on_create(sale, sale_item)
            finally:
                sale_item.quantity = original_quantity
        elif quantity_diff < 0:
            original_quantity = sale_item.quantity
            sale_item.quantity = -quantity_diff
            try:
                self._update_stock_on_delete(sale, sale_item)
            finally:
                sale_item.quantity = original_quantity

    def _update_stock_on_delete(self, sale, sale_item):
        if self._is_stock_transfer_sale(sale):
            return
        qty_to_add = int(sale_item.quantity or 0)
        if qty_to_add <= 0:
            return

        stocks = Stock.objects.select_for_update().filter(
            shop=sale.shop,
            is_deleted=False,
        )

        if sale_item.purchase_item_id:
            stock = stocks.filter(purchase_item_id=sale_item.purchase_item_id).first()
        else:
            stock = None

        if stock is None and sale_item.product_id:
            stock = stocks.filter(purchase_item__product_id=sale_item.product_id).first()

        if stock:
            old_qty = Decimal(str(stock.quantity or 0))
            incoming_qty = Decimal(str(qty_to_add))
            incoming_cost = Decimal(str(sale_item.buy_price or sale_item.purchase_item.buy_price or 0))
            total_qty = old_qty + incoming_qty
            if total_qty > 0:
                stock.average_buy_price = (
                    (old_qty * Decimal(str(stock.average_buy_price or 0)))
                    + (incoming_qty * incoming_cost)
                ) / total_qty
            stock.quantity = int(stock.quantity or 0) + qty_to_add
            stock.sale_price = sale_item.sale_price or stock.sale_price
            stock.save(update_fields=["quantity", "average_buy_price", "sale_price", "updated_at"])
            return

        if sale_item.purchase_item:
            Stock.objects.create(
                shop=sale.shop,
                purchase_item=sale_item.purchase_item,
                quantity=qty_to_add,
                average_buy_price=sale_item.buy_price or sale_item.purchase_item.buy_price or 0,
                average_wholesale_price=sale_item.purchase_item.wholesale_price,
                average_retail_price=sale_item.purchase_item.retail_price,
                sale_price=sale_item.sale_price or sale_item.purchase_item.sale_price or 0,
                wholesale_price=sale_item.purchase_item.wholesale_price,
                retail_price=sale_item.purchase_item.retail_price,
                created_by=sale.created_by,
            )
