from decimal import Decimal, ROUND_UP

from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import Sum
from django.db.models.functions import Coalesce

from shop_management.models.data.account_transaction import AccountTransaction
from shop_management.models.data.customer_credit_application import CustomerCreditApplication
from shop_management.models.data.customer_payment import CustomerPayment
from shop_management.models.data.sale import Sale


def _already_applied(source):
    return source.credit_applications.filter(
        sale__document_status=Sale.DocumentStatus.POSTED
    ).aggregate(total=Sum("source_amount"))["total"] or Decimal("0")


def _available_credit(source):
    return max(
        Decimal("0"),
        Decimal(str(source.unallocated_amount or 0)) - _already_applied(source),
    )


@transaction.atomic
def apply_customer_credit_to_sale(sale_id, *, user):
    trigger_sale = (
        Sale.objects.select_for_update()
        .select_related("customer", "shop", "currency")
        .get(pk=sale_id)
    )
    if trigger_sale.document_status != Sale.DocumentStatus.POSTED:
        raise ValidationError("Customer credit can only be applied to a posted sale.")
    if not trigger_sale.customer_id:
        return trigger_sale

    sources = []
    for payment in trigger_sale.customer.customer_payments.select_for_update().filter(
        status=CustomerPayment.Status.POSTED
    ).select_related("currency"):
        sources.append((payment.created_at, payment.id, "customer_payment", payment))
    for payment in (
        trigger_sale.customer.balance_payments.select_for_update()
        .select_related("currency")
    ):
        sources.append((payment.created_at, payment.id, "balance_payment", payment))
    sources.sort(key=lambda row: (row[0], row[1], row[2]))

    unpaid_sales = (
        Sale.objects.select_for_update()
        .filter(
            customer_id=trigger_sale.customer_id,
            shop_id=trigger_sale.shop_id,
            document_status=Sale.DocumentStatus.POSTED,
            currency__isnull=False,
        )
        .select_related("currency", "shop", "customer")
        .annotate(credit_order_date=Coalesce("invoice_date", "sale_date"))
        .order_by("credit_order_date", "id")
    )

    for sale in unpaid_sales:
        currency_title = sale.currency.title
        remaining = max(
            Decimal("0"),
            Decimal(str(sale.total_remained_amount.get(currency_title, 0) or 0)),
        )
        if remaining <= 0:
            continue

        effective_date = sale.invoice_date or sale.sale_date
        for _, _, source_kind, source in sources:
            if remaining <= 0:
                break
            if not source.currency_id:
                continue
            available = _available_credit(source)
            if available <= 0:
                continue
            rate = AccountTransaction.resolve_exchange_rate(
                sale.shop,
                source.currency_id,
                sale.currency_id,
                effective_date=effective_date,
            )
            if not rate or rate <= 0:
                continue
            converted_available = AccountTransaction.compute_converted_amount(
                available, rate
            )
            if converted_available <= remaining:
                source_amount = available
                sale_amount = converted_available
            else:
                source_amount = min(
                    available,
                    (remaining / rate).quantize(
                        Decimal("0.01"), rounding=ROUND_UP
                    ),
                )
                sale_amount = min(
                    remaining,
                    AccountTransaction.compute_converted_amount(
                        source_amount, rate
                    ),
                )
            if source_amount <= 0 or sale_amount <= 0:
                continue
            CustomerCreditApplication.objects.create(
                customer=sale.customer,
                sale=sale,
                customer_payment=(
                    source if source_kind == "customer_payment" else None
                ),
                balance_payment=(
                    source if source_kind == "balance_payment" else None
                ),
                source_currency=source.currency,
                sale_currency=sale.currency,
                source_amount=source_amount,
                sale_amount=sale_amount,
                exchange_rate=rate,
                created_by=user,
            )
            remaining -= sale_amount

    return trigger_sale