from datetime import timedelta
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
from django.db import models, transaction
from django.utils import timezone
from rest_framework import serializers
from shop_management.models.data.utils import Currency, classify_currency_code
from shop_management.serializers.data.base import DataRootSerializer
from shop_management.models.data.customer import Customer
from shop_management.models.data.customer_balance_payment import CustomerBalancePayment, CustomerBalancePaymentAllocation
from shop_management.models.data.account_transaction import (
    AccountTransaction,
    AccountTransactionType,
)
from shop_management.models.data.shop_account import Account
from shop_management.models.data.sale import Sale
from shop_management.models.data.customer_debtor import CustomerDebtor


def _quantize_rate_value(val):
    if val is None or val == "":
        return val
    try:
        d = Decimal(str(val))
        return d.quantize(Decimal("0.000001"), rounding=ROUND_HALF_UP)
    except (InvalidOperation, ValueError, TypeError):
        return val



class CustomerBalancePaymentAllocationSerializer(serializers.ModelSerializer):
    target_currency_details = serializers.SerializerMethodField()

    class Meta:
        model = CustomerBalancePaymentAllocation
        fields = "__all__"

    def get_target_currency_details(self, obj):
        if obj.target_currency:
            return {"id": obj.target_currency.id, "title": obj.target_currency.title}
        return None


class CustomerBalancePaymentSerializer(DataRootSerializer):
    allocations = CustomerBalancePaymentAllocationSerializer(many=True, read_only=True)
    customer_details = serializers.SerializerMethodField()
    currency_details = serializers.SerializerMethodField()
    settlement_currency_details = serializers.SerializerMethodField()
    account_details = serializers.SerializerMethodField()
    supplier_branch_details = serializers.SerializerMethodField()
    customer_branch_details = serializers.SerializerMethodField()
    shop = serializers.ReadOnlyField(source="customer.shop_id")
    shop_name = serializers.ReadOnlyField(source="customer.shop.name")
    apply_to_opening_balance = serializers.BooleanField(write_only=True, required=False, default=False)

    class Meta:
        model = CustomerBalancePayment
        fields = "__all__"
        read_only_fields = [
            "converted_amount",
            "unallocated_amount",
            "allocated_to_afn",
            "allocated_to_usd",
            "allocated_to_opening_afn",
            "allocated_to_opening_usd",
            "is_void",
            "voided_at",
            "voided_by",
            "void_reason",
        ]

    def to_internal_value(self, data):
        self._raw_exchange_rates = {}
        if hasattr(data, "copy"):
            mutable = data.copy()
        elif isinstance(data, dict):
            mutable = dict(data)
        else:
            mutable = data

        if isinstance(mutable, dict):
            req = self.context.get("request")
            if req and not mutable.get("client_reference_id"):
                idemp = req.headers.get("Idempotency-Key") or req.headers.get("X-Idempotency-Key")
                if idemp:
                    mutable["client_reference_id"] = idemp
            for key in ("exchange_rate", "settlement_exchange_rate", "secondary_exchange_rate"):
                if key in mutable and mutable[key] not in (None, ""):
                    try:
                        self._raw_exchange_rates[key] = Decimal(str(mutable[key]))
                        mutable[key] = _quantize_rate_value(mutable[key])
                    except (InvalidOperation, ValueError, TypeError):
                        pass
        return super().to_internal_value(mutable)

    def get_customer_details(self, obj):
        if obj.customer:
            name = (obj.customer.name or "").strip()
            if not name and obj.customer.email:
                name = str(obj.customer.email).split("@")[0].strip()
            if not name and obj.customer.contact:
                name = str(obj.customer.contact).strip()
            if not name:
                name = "Unnamed Customer"
            return {"id": obj.customer.id, "name": name}
        return None

    def get_currency_details(self, obj):
        if obj.currency:
            return {"id": obj.currency.id, "title": obj.currency.title}
        return None

    def get_settlement_currency_details(self, obj):
        if obj.settlement_currency:
            return {"id": obj.settlement_currency.id, "title": obj.settlement_currency.title}
        if obj.currency:
            return {"id": obj.currency.id, "title": obj.currency.title}
        return None

    def get_account_details(self, obj):
        if obj.account:
            return {
                "id": obj.account.id,
                "name": obj.account.name,
                "currency_id": obj.account.currency_id,
                "currency_title": obj.account.currency.title if obj.account.currency else None,
            }
        return None

    def get_supplier_branch_details(self, obj):
        if obj.supplier_branch:
            return {"id": obj.supplier_branch.id, "name": obj.supplier_branch.name}
        return None

    def get_customer_branch_details(self, obj):
        if obj.customer_branch:
            return {"id": obj.customer_branch.id, "name": obj.customer_branch.name}
        return None

    @staticmethod
    def _get_or_create_clearing_account(shop, currency):
        """
        Auto-create (or reuse) one clearing account per shop/currency.
        """
        name = f"Sale {currency.title} Clearing"
        account, _ = Account.objects.get_or_create(
            shop=shop,
            name=name,
            defaults={"currency": currency, "opening_balance": Decimal("0"), "is_system_account": True},
        )
        updates = []
        if account.currency_id != currency.id:
            account.currency = currency
            updates.append("currency")
        if not account.is_system_account:
            account.is_system_account = True
            updates.append("is_system_account")
        if updates:
            account.save(update_fields=updates)
        return account

    @staticmethod
    def _payment_ref_prefix(payment):
        return f"Customer balance payment #{payment.id} from {payment.customer.name}"

    def _sync_account_transactions(self, payment):
        if not payment.customer or not payment.customer.shop_id:
            raise serializers.ValidationError(
                {"customer": "Selected customer must belong to a shop."}
            )
        if not payment.account or not payment.account.currency_id:
            raise serializers.ValidationError(
                {"account": "Selected account must have a currency."}
            )
        if not payment.currency_id:
            raise serializers.ValidationError(
                {"currency": "Currency is required."}
            )
        if payment.account.shop_id != payment.customer.shop_id:
            raise serializers.ValidationError(
                {"account": "Selected account must belong to the same shop as customer."}
            )
        if payment.currency.shop_id != payment.customer.shop_id:
            raise serializers.ValidationError(
                {"currency": "Currency must belong to the same shop as customer."}
            )

        ref_prefix = self._payment_ref_prefix(payment)
        discount = Decimal(str(getattr(payment, "discount", 0) or 0))
        discount_note = f" | Discount: {discount}" if discount > 0 else ""

        # Destination account: payment inflow (cash only — discount is not cash)
        AccountTransaction.objects.create(
            account=payment.account,
            account_currency=payment.account.currency,
            transaction_currency=payment.currency,
            transaction_type=AccountTransactionType.CREDIT,
            amount=payment.amount,
            exchange_rate=payment.exchange_rate,
            converted_amount=payment.converted_amount,
            description=(
                f"{ref_prefix}{discount_note}"
                f"{f' | Notes: {payment.notes}' if payment.notes else ''}"
            ),
            transaction_date=payment.payment_date.date(),
            source_type="CUSTOMER_BALANCE_PAYMENT",
            source_id=str(payment.id),
            is_system_generated=True,
        )

        # Clearing account: mirror outflow in sale-payment currency (minus)
        clearing_account = self._get_or_create_clearing_account(
            payment.customer.shop, payment.currency
        )
        AccountTransaction.objects.create(
            account=clearing_account,
            account_currency=clearing_account.currency,
            transaction_currency=payment.currency,
            transaction_type=AccountTransactionType.DEBIT,
            amount=payment.amount,
            exchange_rate=Decimal("1"),
            converted_amount=payment.amount,
            description=f"{ref_prefix} | Clearing mirror",
            transaction_date=payment.payment_date.date(),
            source_type="CUSTOMER_BALANCE_PAYMENT",
            source_id=str(payment.id),
            is_system_generated=True,
        )

    def validate(self, attrs):
        if self.instance is not None:
            if getattr(self.instance, "is_void", False):
                raise serializers.ValidationError("Cannot edit a cancelled balance payment.")
            financial_fields = [
                "amount", "discount", "currency", "settlement_currency",
                "account", "exchange_rate", "settlement_exchange_rate",
                "secondary_exchange_rate", "customer", "payment_date",
            ]
            changed_fields = []
            for field in financial_fields:
                if field in attrs:
                    old_val = getattr(self.instance, field)
                    new_val = attrs[field]
                    old_pk = getattr(old_val, "pk", old_val)
                    new_pk = getattr(new_val, "pk", new_val)
                    if field in ("amount", "discount", "exchange_rate", "settlement_exchange_rate", "secondary_exchange_rate"):
                        try:
                            old_num = Decimal(str(old_val or 0)) if old_val is not None else None
                            new_num = Decimal(str(new_val or 0)) if new_val is not None else None
                            if old_num != new_num:
                                changed_fields.append(field)
                        except Exception:
                            if old_val != new_val:
                                changed_fields.append(field)
                    elif old_pk != new_pk:
                        changed_fields.append(field)
            if changed_fields:
                raise serializers.ValidationError({
                    "detail": (
                        f"Posted balance payments cannot be modified ({', '.join(changed_fields)} changed). "
                        "Cancel this payment and record a new one instead."
                    )
                })

        payment_date = attrs.get("payment_date")
        if payment_date is not None and timezone.is_naive(payment_date):
            attrs["payment_date"] = timezone.make_aware(
                payment_date, timezone.get_current_timezone()
            )

        raw_rates = getattr(self, "_raw_exchange_rates", {})
        customer = attrs.get("customer") or getattr(self.instance, "customer", None)
        currency = attrs.get("currency") or getattr(self.instance, "currency", None)
        settlement_currency = attrs.get("settlement_currency") or getattr(
            self.instance, "settlement_currency", None
        )
        account = attrs.get("account") or getattr(self.instance, "account", None)
        supplier_branch = attrs.get("supplier_branch") or getattr(self.instance, "supplier_branch", None)
        customer_branch = attrs.get("customer_branch") or getattr(self.instance, "customer_branch", None)
        amount = attrs.get("amount") or getattr(self.instance, "amount", None)
        discount = attrs.get("discount", getattr(self.instance, "discount", Decimal("0")))
        manual_rate = raw_rates.get("exchange_rate", attrs.get("exchange_rate"))
        manual_settlement_rate = raw_rates.get(
            "settlement_exchange_rate", attrs.get("settlement_exchange_rate")
        )
        manual_secondary_rate = raw_rates.get(
            "secondary_exchange_rate", attrs.get("secondary_exchange_rate")
        )
        use_branch_fields = bool(supplier_branch or customer_branch)

        if not customer:
            raise serializers.ValidationError({"customer": "Customer is required."})

        if self.instance is None:
            client_ref = attrs.get("client_reference_id")
            if not client_ref:
                req = self.context.get("request")
                if req:
                    client_ref = req.headers.get("Idempotency-Key") or req.headers.get("X-Idempotency-Key")
                    if client_ref:
                        attrs["client_reference_id"] = client_ref

            if client_ref and customer:
                if CustomerBalancePayment.objects.filter(
                    customer=customer,
                    client_reference_id=client_ref,
                    is_void=False,
                ).exists():
                    raise serializers.ValidationError({
                        "client_reference_id": f"A payment with reference '{client_ref}' has already been processed."
                    })

        if not currency:
            raise serializers.ValidationError({"currency": "Currency is required."})
        if not settlement_currency:
            settlement_currency = currency
            attrs["settlement_currency"] = currency
        if not amount or amount <= 0:
            raise serializers.ValidationError({"amount": "Amount must be greater than 0."})
        try:
            discount = Decimal(str(discount or 0))
        except Exception:
            raise serializers.ValidationError({"discount": "Invalid discount amount."})
        if discount < 0:
            raise serializers.ValidationError({"discount": "Discount cannot be negative."})
        attrs["discount"] = discount
        if currency.shop_id != customer.shop_id:
            raise serializers.ValidationError(
                {"currency": "Currency must belong to the customer shop."}
            )
        if settlement_currency.shop_id != customer.shop_id:
            raise serializers.ValidationError(
                {"settlement_currency": "Debt currency must belong to the customer shop."}
            )

        if account is None:
            raise serializers.ValidationError({"account": "Account is required."})
        if account.shop_id != customer.shop_id:
            raise serializers.ValidationError(
                {"account": "Selected account must belong to the same shop as customer."}
            )
        if use_branch_fields:
            if not supplier_branch or not customer_branch:
                raise serializers.ValidationError(
                    {"supplier_branch": "Supplier and customer branches are required together."}
                )
            if supplier_branch.id == customer_branch.id:
                raise serializers.ValidationError(
                    {"customer_branch": "Supplier and customer branch must be different."}
                )
            if supplier_branch.shop_id != customer.shop_id or customer_branch.shop_id != customer.shop_id:
                raise serializers.ValidationError(
                    {"customer_branch": "Branch must belong to the same shop as the customer."}
                )

        if manual_secondary_rate is not None and manual_secondary_rate <= 0:
            raise serializers.ValidationError(
                {"secondary_exchange_rate": "Secondary exchange rate must be greater than 0."}
            )

        effective_date = payment_date.date() if payment_date else None
        db_account_rate = AccountTransaction.resolve_exchange_rate(
            customer.shop, currency.id, account.currency_id, effective_date
        )
        resolved_rate = manual_rate or db_account_rate
        if resolved_rate is None and account.currency_id == settlement_currency.id and manual_settlement_rate:
            resolved_rate = manual_settlement_rate
        if resolved_rate is None:
            raise serializers.ValidationError(
                {"account": "No exchange rate found between currency and account currency."}
            )
        if resolved_rate <= 0:
            raise serializers.ValidationError(
                {"exchange_rate": "Exchange rate must be greater than 0."}
            )

        attrs["exchange_rate"] = _quantize_rate_value(resolved_rate)
        attrs["converted_amount"] = AccountTransaction.compute_converted_amount(amount, resolved_rate)

        db_settlement_rate = AccountTransaction.resolve_exchange_rate(
            customer.shop,
            currency.id,
            settlement_currency.id,
            effective_date,
        )
        if currency.id == settlement_currency.id:
            settlement_rate = manual_settlement_rate or Decimal("1")
        else:
            settlement_rate = manual_settlement_rate or db_settlement_rate
            if (
                settlement_rate is not None
                and db_settlement_rate is not None
                and _quantize_rate_value(settlement_rate) == _quantize_rate_value(db_settlement_rate)
            ):
                settlement_rate = db_settlement_rate

        if settlement_rate is None or settlement_rate <= 0:
            raise serializers.ValidationError(
                {
                    "settlement_exchange_rate": (
                        f"No exchange rate found from {currency.title} to "
                        f"{settlement_currency.title}."
                    )
                }
            )
        attrs["settlement_exchange_rate"] = _quantize_rate_value(settlement_rate)
        attrs["settlement_amount"] = AccountTransaction.compute_converted_amount(
            amount, settlement_rate
        )
        return attrs

    @staticmethod
    def _apply_to_currency_debt_detailed(customer, currency_id, amount_to_apply, apply_to_opening=False):
        """
        Applies debt reduction amount to a specific currency for the given customer:
        1) Remaining opening balance in this currency (preserving original opening balance)
        2) Unpaid sales in currency (oldest first)
        3) Outstanding debtor loans in currency (oldest first)
        Returns tuple: (leftover_amount, applied_to_opening_amount, allocations_list)
        """
        amount_to_apply = Decimal(str(amount_to_apply or 0))
        if amount_to_apply <= 0 or not customer or not currency_id:
            return (
                amount_to_apply if amount_to_apply > 0 else Decimal("0"),
                Decimal("0"),
                [],
            )

        applied_opening = Decimal("0")
        allocations = []
        curr_obj = Currency.objects.filter(pk=currency_id).first()
        curr_code = classify_currency_code(curr_obj)

        # 1) Opening balance (preserve original opening_balance_afn / opening_balance_usd)
        if apply_to_opening:
            unallocated_cp = sum(
                Decimal(str(p.unallocated_amount or 0))
                for p in customer.customer_payments.filter(status="POSTED", currency_id=currency_id)
            )
            if curr_code == "AFN":
                if (
                    Decimal(str(customer.opening_balance_afn or 0)) == 0
                    and Decimal(str(customer.opening_balance_afn_paid or 0)) == 0
                    and Decimal(str(customer.opening_balance or 0)) > 0
                    and customer.opening_balance_currency_id == currency_id
                ):
                    customer.opening_balance_afn = Decimal(str(customer.opening_balance))
                rem_opening = customer.remaining_opening_balance_afn
                net_opening = max(Decimal("0"), rem_opening - unallocated_cp)
                if net_opening > 0:
                    apply_opening = min(net_opening, amount_to_apply)
                    customer.opening_balance_afn_paid = (
                        Decimal(str(customer.opening_balance_afn_paid or 0)) + apply_opening
                    )
                    update_fields = ["opening_balance_afn", "opening_balance_afn_paid"]
                    if customer.opening_balance_currency_id == currency_id or not customer.opening_balance_currency_id:
                        customer.opening_balance = max(
                            Decimal("0"), Decimal(str(customer.opening_balance or 0)) - apply_opening
                        )
                        update_fields.append("opening_balance")
                    customer.save(update_fields=update_fields)
                    amount_to_apply -= apply_opening
                    applied_opening += apply_opening
                    allocations.append({
                        "target_type": "OPENING_BALANCE",
                        "target_id": customer.id,
                        "target_currency_id": currency_id,
                        "allocated_amount": apply_opening,
                    })
            elif curr_code == "USD":
                if (
                    Decimal(str(customer.opening_balance_usd or 0)) == 0
                    and Decimal(str(customer.opening_balance_usd_paid or 0)) == 0
                    and Decimal(str(customer.opening_balance or 0)) > 0
                    and customer.opening_balance_currency_id == currency_id
                ):
                    customer.opening_balance_usd = Decimal(str(customer.opening_balance))
                rem_opening = customer.remaining_opening_balance_usd
                net_opening = max(Decimal("0"), rem_opening - unallocated_cp)
                if net_opening > 0:
                    apply_opening = min(net_opening, amount_to_apply)
                    customer.opening_balance_usd_paid = (
                        Decimal(str(customer.opening_balance_usd_paid or 0)) + apply_opening
                    )
                    update_fields = ["opening_balance_usd", "opening_balance_usd_paid"]
                    if customer.opening_balance_currency_id == currency_id or not customer.opening_balance_currency_id:
                        customer.opening_balance = max(
                            Decimal("0"), Decimal(str(customer.opening_balance or 0)) - apply_opening
                        )
                        update_fields.append("opening_balance")
                    customer.save(update_fields=update_fields)
                    amount_to_apply -= apply_opening
                    applied_opening += apply_opening
                    allocations.append({
                        "target_type": "OPENING_BALANCE",
                        "target_id": customer.id,
                        "target_currency_id": currency_id,
                        "allocated_amount": apply_opening,
                    })
            elif customer.opening_balance_currency_id == currency_id:
                opening = Decimal(str(customer.opening_balance or 0))
                net_opening = max(Decimal("0"), opening - unallocated_cp)
                if net_opening > 0:
                    apply_opening = min(net_opening, amount_to_apply)
                    customer.opening_balance = opening - apply_opening
                    customer.save(update_fields=["opening_balance"])
                    amount_to_apply -= apply_opening
                    applied_opening += apply_opening
                    allocations.append({
                        "target_type": "OPENING_BALANCE",
                        "target_id": customer.id,
                        "target_currency_id": currency_id,
                        "allocated_amount": apply_opening,
                    })

        # 2) Unpaid sales (oldest first)
        if amount_to_apply > 0:
            sales_qs = (
                Sale.objects.select_for_update()
                .filter(
                    customer_id=customer.id,
                    currency_id=currency_id,
                )
                .filter(
                    models.Q(uses_inventory_engine=False)
                    | models.Q(document_status=Sale.DocumentStatus.POSTED)
                )
                .order_by("sale_date", "id")
            )

            for sale in sales_qs:
                if amount_to_apply <= 0:
                    break
                remained_map = sale.total_remained_amount or {}
                currency_key = sale.currency.title if sale.currency else None
                sale_remaining = Decimal(str(remained_map.get(currency_key, 0) or 0))
                if sale_remaining <= 0:
                    continue

                apply_sale = min(sale_remaining, amount_to_apply)
                current_received = Decimal(sale.received_amount or 0)
                sale.received_amount = current_received + apply_sale
                sale.save(update_fields=["received_amount"])
                sale.update_status()
                amount_to_apply -= apply_sale
                allocations.append({
                    "target_type": "SALE",
                    "target_id": sale.id,
                    "target_currency_id": sale.currency_id,
                    "allocated_amount": apply_sale,
                })

        # 3) Debtor loans (oldest first)
        if amount_to_apply > 0:
            loans = (
                CustomerDebtor.objects.select_for_update()
                .filter(
                    customer_id=customer.id,
                    currency_id=currency_id,
                )
                .order_by("loan_date", "id")
            )
            for loan in loans:
                if amount_to_apply <= 0:
                    break
                outstanding = loan.outstanding_amount
                if outstanding <= 0:
                    continue
                apply_loan = min(outstanding, amount_to_apply)
                loan.settled_amount = Decimal(str(loan.settled_amount or 0)) + apply_loan
                loan.save(update_fields=["settled_amount"])
                amount_to_apply -= apply_loan
                allocations.append({
                    "target_type": "DEBTOR_LOAN",
                    "target_id": loan.id,
                    "target_currency_id": loan.currency_id,
                    "allocated_amount": apply_loan,
                })

        return (
            amount_to_apply if amount_to_apply > 0 else Decimal("0"),
            applied_opening,
            allocations,
        )

    @staticmethod
    def _apply_to_currency_debt(customer, currency_id, amount_to_apply, apply_to_opening=False):
        leftover, _, _ = CustomerBalancePaymentSerializer._apply_to_currency_debt_detailed(
            customer, currency_id, amount_to_apply, apply_to_opening=apply_to_opening
        )
        return leftover

    @transaction.atomic
    def create(self, validated_data):
        apply_to_opening_balance = bool(validated_data.pop("apply_to_opening_balance", False))
        raw_rates = getattr(self, "_raw_exchange_rates", {})
        manual_secondary_rate = raw_rates.get(
            "secondary_exchange_rate", validated_data.get("secondary_exchange_rate")
        )

        customer_obj = validated_data.get("customer")
        if customer_obj:
            customer_obj = Customer.objects.select_for_update().get(pk=customer_obj.pk)
            validated_data["customer"] = customer_obj
            client_ref = validated_data.get("client_reference_id")
            if client_ref:
                if CustomerBalancePayment.objects.filter(
                    customer=customer_obj,
                    client_reference_id=client_ref,
                    is_void=False,
                ).exists():
                    raise serializers.ValidationError({
                        "client_reference_id": f"A payment with reference '{client_ref}' has already been processed."
                    })

        payment = super().create(validated_data)
        self._sync_account_transactions(payment)

        # Converted cash + discount = reduction available in primary debt currency
        remaining = self._payment_apply_total(payment)
        settlement_curr_id = self._settlement_currency_id(payment)
        customer = customer_obj
        audit_notes = []

        allocated_afn = Decimal("0")
        allocated_usd = Decimal("0")
        allocated_opening_afn = Decimal("0")
        allocated_opening_usd = Decimal("0")
        used_secondary_rate = None

        all_allocations = []

        # 1) Settle primary debt currency
        if remaining > 0 and customer and settlement_curr_id:
            initial_remaining = remaining
            remaining, applied_opening, primary_allocs = self._apply_to_currency_debt_detailed(
                customer, settlement_curr_id, remaining, apply_to_opening_balance
            )
            for a in primary_allocs:
                a["exchange_rate"] = payment.settlement_exchange_rate or Decimal("1")
                all_allocations.append(a)
            applied_primary = initial_remaining - remaining
            if applied_primary > 0:
                curr_obj = payment.settlement_currency or payment.currency
                curr_title = curr_obj.title if curr_obj else ""
                curr_code = classify_currency_code(curr_obj)
                if curr_code == "USD":
                    allocated_usd += applied_primary
                    allocated_opening_usd += applied_opening
                else:
                    allocated_afn += applied_primary
                    allocated_opening_afn += applied_opening
                audit_notes.append(f"Settled {applied_primary} {curr_title}")

        # 2) Multi-currency cascade:
        # If surplus remains, check if customer has debts in other currencies
        if remaining > 0 and customer and customer.shop:
            other_debts = customer.get_total_owed_by_currency()
            effective_date = payment.payment_date.date() if payment.payment_date else None

            for curr_title, owed_val in list(other_debts.items()):
                if remaining <= 0:
                    break
                owed_val = Decimal(str(owed_val or 0))
                if owed_val <= 0:
                    continue

                other_curr = Currency.objects.filter(shop=customer.shop, title=curr_title).first()
                if not other_curr or other_curr.id == settlement_curr_id:
                    continue

                # Exchange rate from settlement currency to other debt currency
                db_rate = AccountTransaction.resolve_exchange_rate(
                    customer.shop,
                    settlement_curr_id,
                    other_curr.id,
                    effective_date,
                )
                candidate_manual = manual_secondary_rate
                if candidate_manual is None and payment.currency_id == settlement_curr_id:
                    for fallback_key in ("settlement_exchange_rate", "exchange_rate"):
                        val = raw_rates.get(fallback_key)
                        if val is not None and val > Decimal("0") and val != Decimal("1"):
                            candidate_manual = val
                            break
                if candidate_manual is not None and candidate_manual > Decimal("0"):
                    from_code = classify_currency_code(payment.settlement_currency or payment.currency)
                    to_code = classify_currency_code(other_curr)
                    if from_code == "AFN" and to_code == "USD" and candidate_manual > Decimal("1"):
                        rate = Decimal("1") / candidate_manual
                    elif from_code == "USD" and to_code == "AFN" and candidate_manual < Decimal("1"):
                        rate = Decimal("1") / candidate_manual
                    else:
                        rate = candidate_manual
                else:
                    rate = db_rate
                if (
                    rate is not None
                    and db_rate is not None
                    and _quantize_rate_value(rate) == _quantize_rate_value(db_rate)
                ):
                    rate = db_rate
                if not rate or rate <= 0:
                    if payment.currency_id and payment.currency_id != settlement_curr_id:
                        rate_pay_to_other = AccountTransaction.resolve_exchange_rate(
                            customer.shop, payment.currency_id, other_curr.id, effective_date
                        )
                        raw_sr = raw_rates.get("settlement_exchange_rate", payment.settlement_exchange_rate)
                        if rate_pay_to_other and rate_pay_to_other > 0 and raw_sr:
                            rate = rate_pay_to_other / Decimal(str(raw_sr))

                if not rate or rate <= 0:
                    continue

                # Capacity in other_curr
                capacity_in_other = AccountTransaction.compute_converted_amount(remaining, rate)
                settle_in_other = min(owed_val, capacity_in_other)
                if settle_in_other <= 0:
                    continue

                # Apply settle_in_other to other_curr debts
                unapplied_other, applied_opening_other, sec_allocs = self._apply_to_currency_debt_detailed(
                    customer, other_curr.id, settle_in_other, apply_to_opening_balance
                )
                for a in sec_allocs:
                    a["exchange_rate"] = rate
                    all_allocations.append(a)
                actual_cleared_in_other = settle_in_other - unapplied_other
                actual_consumed = (actual_cleared_in_other / rate).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
                if actual_consumed > remaining:
                    actual_consumed = remaining

                remaining -= actual_consumed
                if actual_cleared_in_other > 0:
                    used_secondary_rate = _quantize_rate_value(rate)
                    other_code = classify_currency_code(other_curr)
                    if other_code == "USD":
                        allocated_usd += actual_cleared_in_other
                        allocated_opening_usd += applied_opening_other
                    else:
                        allocated_afn += actual_cleared_in_other
                        allocated_opening_afn += applied_opening_other
                    settlement_title = payment.settlement_currency.title if payment.settlement_currency else ""
                    audit_notes.append(
                        f"Cleared {Decimal(str(actual_cleared_in_other)):.2f} {other_curr.title} using {Decimal(str(actual_consumed)):.2f} {settlement_title}"
                    )

        # 3) Customer Credit:
        # Any remaining surplus after settling all debts across all currencies is customer credit!
        if remaining > 0:
            raw_settlement_rate = raw_rates.get("settlement_exchange_rate", payment.settlement_exchange_rate)
            if raw_settlement_rate and raw_settlement_rate > 0 and payment.currency_id != settlement_curr_id:
                unallocated_cash = (remaining / Decimal(str(raw_settlement_rate))).quantize(
                    Decimal("0.01"), rounding=ROUND_HALF_UP
                )
            else:
                unallocated_cash = remaining.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)

            payment.unallocated_amount = unallocated_cash
            pay_curr_title = payment.currency.title if payment.currency else ""
            audit_notes.append(f"Credit: {unallocated_cash} {pay_curr_title}")
        else:
            payment.unallocated_amount = Decimal("0")

        payment.allocated_to_afn = allocated_afn.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
        payment.allocated_to_usd = allocated_usd.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
        payment.allocated_to_opening_afn = allocated_opening_afn.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
        payment.allocated_to_opening_usd = allocated_opening_usd.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
        if used_secondary_rate is not None:
            payment.secondary_exchange_rate = used_secondary_rate

        if audit_notes:
            note_addition = " | " + " | ".join(audit_notes)
            payment.notes = ((payment.notes or "").strip() + note_addition).strip(" |")

        payment.save(
            update_fields=[
                "unallocated_amount",
                "allocated_to_afn",
                "allocated_to_usd",
                "allocated_to_opening_afn",
                "allocated_to_opening_usd",
                "secondary_exchange_rate",
                "notes",
            ]
        )

        if all_allocations:
            CustomerBalancePaymentAllocation.objects.bulk_create([
                CustomerBalancePaymentAllocation(
                    payment=payment,
                    target_type=a["target_type"],
                    target_id=a["target_id"],
                    target_currency_id=a["target_currency_id"],
                    allocated_amount=a["allocated_amount"],
                    exchange_rate=a.get("exchange_rate", Decimal("1")),
                )
                for a in all_allocations
            ])

        return payment

    @transaction.atomic
    def update(self, instance, validated_data):
        # Posted payments cannot have financial fields modified. Only notes can be updated.
        if "notes" in validated_data:
            instance.notes = validated_data["notes"]
            instance.save(update_fields=["notes"])
        return instance

    @staticmethod
    def _payment_apply_total(payment):
        """Converted cash + discount = reduction in the debt currency."""
        cash = payment.settlement_amount or payment.amount or 0
        return Decimal(str(cash)) + Decimal(str(getattr(payment, "discount", 0) or 0))

    @staticmethod
    def _settlement_currency_id(payment):
        return payment.settlement_currency_id or payment.currency_id

    @staticmethod
    def _apply_payment_to_sales(payment, remaining_to_apply=None):
        settlement_curr_id = CustomerBalancePaymentSerializer._settlement_currency_id(payment)
        if remaining_to_apply is None:
            remaining_to_apply = CustomerBalancePaymentSerializer._payment_apply_total(payment)
        return CustomerBalancePaymentSerializer._apply_to_currency_debt(
            payment.customer, settlement_curr_id, remaining_to_apply, apply_to_opening=False
        )

    @staticmethod
    def _apply_payment_to_debtors(payment, remaining_to_apply=None):
        settlement_curr_id = CustomerBalancePaymentSerializer._settlement_currency_id(payment)
        if remaining_to_apply is None:
            remaining_to_apply = CustomerBalancePaymentSerializer._payment_apply_total(payment)
        return CustomerBalancePaymentSerializer._apply_to_currency_debt(
            payment.customer, settlement_curr_id, remaining_to_apply, apply_to_opening=False
        )

    @staticmethod
    def _apply_payment_to_opening_balance(payment, remaining_to_apply=None):
        settlement_curr_id = CustomerBalancePaymentSerializer._settlement_currency_id(payment)
        if remaining_to_apply is None:
            remaining_to_apply = CustomerBalancePaymentSerializer._payment_apply_total(payment)
        return CustomerBalancePaymentSerializer._apply_to_currency_debt(
            payment.customer, settlement_curr_id, remaining_to_apply, apply_to_opening=True
        )
