from decimal import Decimal

from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.db.models.functions import Coalesce
from django.utils import timezone

from shop_management.models.data.account_transaction import AccountTransaction, AccountTransactionType
from shop_management.models.data.customer_payment import CustomerPayment, CustomerPaymentAllocation
from shop_management.models.data.sale import Sale
from shop_management.services.automatic_journals import create_system_journal, financial_ledger_code, void_system_journal


def _sale_due(sale, payment):
    if sale.uses_inventory_engine:
        gross = sale.calculated_total
    else:
        gross = sum((Decimal(str(value)) for value in sale.effective_total_amount.values()), Decimal("0"))
    legacy = (sale.received_amount or Decimal("0")) + sum((row.amount for row in sale.payments.all()), Decimal("0"))
    allocated = sum((row.allocated_amount for row in sale.customer_payment_allocations.filter(payment__status=CustomerPayment.Status.POSTED).exclude(payment=payment)), Decimal("0"))
    returns = sale.sale_returns.filter(
        models.Q(uses_inventory_engine=False) | models.Q(document_status="POSTED")
    ).aggregate(total=models.Sum("total_refound_amount"))["total"] or Decimal("0")
    return gross - legacy - allocated - returns


def _validate(payment):
    errors = {}
    if payment.customer.shop_id != payment.shop_id:
        errors["customer"] = "Customer must belong to the payment branch."
    if payment.financial_account.shop_id != payment.shop_id:
        errors["financial_account"] = "Account must belong to the payment branch."
    if payment.currency.shop_id != payment.shop_id:
        errors["currency"] = "Currency must belong to the payment branch."
    if payment.amount <= 0 or payment.exchange_rate_used <= 0:
        errors["amount"] = "Amount and exchange rate must be greater than zero."
    allocated = Decimal("0")
    for row in payment.allocations.select_related("sale"):
        if row.allocated_amount <= 0:
            errors[f"allocation_{row.pk}"] = "Allocation must be greater than zero."
        if row.sale.document_status != Sale.DocumentStatus.POSTED:
            errors[f"allocation_{row.pk}"] = "Only posted sales may be paid."
        if row.sale.shop_id != payment.shop_id or row.sale.customer_id != payment.customer_id:
            errors[f"allocation_{row.pk}"] = "Sale must belong to the same branch and customer."
        if row.sale.currency_id != payment.currency_id:
            errors[f"allocation_{row.pk}"] = "Sale currency must match payment currency."
        due = _sale_due(row.sale, payment)
        if row.allocated_amount > due:
            errors[f"allocation_{row.pk}"] = f"Allocation exceeds outstanding amount {due}."
        allocated += row.allocated_amount
    if allocated > payment.amount:
        errors["allocations"] = "Total allocations cannot exceed the payment amount."
    if errors:
        raise ValidationError(errors)


@transaction.atomic
def allocate_customer_payment_fifo(payment_id, *, user):
    payment = CustomerPayment.objects.select_for_update().select_related(
        "customer", "shop", "currency"
    ).get(pk=payment_id)
    payment.allocations.all().delete()
    remaining = Decimal(str(payment.amount or 0))

    sales = (
        Sale.objects.select_for_update()
        .filter(
            customer_id=payment.customer_id,
            shop_id=payment.shop_id,
            currency_id=payment.currency_id,
            document_status=Sale.DocumentStatus.POSTED,
        )
        .annotate(allocation_date=Coalesce("invoice_date", "sale_date"))
        .order_by("allocation_date", "id")
    )
    for sale in sales:
        if remaining <= 0:
            break
        due = max(_sale_due(sale, payment), Decimal("0"))
        allocated_amount = min(due, remaining)
        if allocated_amount <= 0:
            continue
        CustomerPaymentAllocation.objects.create(
            payment=payment,
            sale=sale,
            allocated_amount=allocated_amount,
            created_by=user,
        )
        remaining -= allocated_amount

    return payment


@transaction.atomic
def post_customer_payment(payment_id, *, user):
    payment = CustomerPayment.objects.select_for_update().select_related("customer", "shop", "financial_account__currency", "currency").get(pk=payment_id)
    if payment.status != CustomerPayment.Status.DRAFT:
        raise ValidationError("Only draft customer payments can be posted.")
    _validate(payment)
    payment.base_amount = AccountTransaction.compute_converted_amount(payment.amount, payment.exchange_rate_used)
    AccountTransaction.objects.create(
        account=payment.financial_account, account_currency=payment.financial_account.currency,
        transaction_currency=payment.currency, transaction_type=AccountTransactionType.CREDIT,
        amount=payment.amount, exchange_rate=payment.exchange_rate_used,
        converted_amount=payment.base_amount, transaction_date=payment.payment_date,
        description=f"Customer payment #{payment.pk} | {payment.reference}",
        reference=payment.reference, source_type="CUSTOMER_PAYMENT", source_id=str(payment.pk),
        is_system_generated=True, created_by=user,
    )
    payment.status = CustomerPayment.Status.POSTED
    payment.posted_at = timezone.now()
    payment.posted_by = user
    payment.save(update_fields=["base_amount", "status", "posted_at", "posted_by", "updated_at"])
    create_system_journal(shop=payment.shop, entry_date=payment.payment_date, source_type="CUSTOMER_PAYMENT", source_id=payment.pk,
        description=f"Customer receipt {payment.reference or payment.pk}", currency=payment.currency, exchange_rate=payment.exchange_rate_used, user=user,
        lines=[{"code": financial_ledger_code(payment.financial_account), "side": "debit", "amount": payment.amount, "financial_account": payment.financial_account, "customer": payment.customer}, {"code": "1400", "side": "credit", "amount": payment.amount, "customer": payment.customer}])
    return payment


@transaction.atomic
def void_customer_payment(payment_id, *, user, reason):
    payment = CustomerPayment.objects.select_for_update().select_related("financial_account__currency", "currency").get(pk=payment_id)
    if payment.status != CustomerPayment.Status.POSTED:
        raise ValidationError("Only posted customer payments can be cancelled.")
    if not str(reason or "").strip():
        raise ValidationError({"void_reason": "A void reason is required."})
    AccountTransaction.objects.create(
        account=payment.financial_account, account_currency=payment.financial_account.currency,
        transaction_currency=payment.currency, transaction_type=AccountTransactionType.DEBIT,
        amount=payment.amount, exchange_rate=payment.exchange_rate_used,
        converted_amount=payment.base_amount, transaction_date=timezone.localdate(),
        description=f"Customer payment void #{payment.pk} | {str(reason).strip()}",
        reference=payment.reference, source_type="CUSTOMER_PAYMENT_VOID", source_id=str(payment.pk),
        is_system_generated=True, created_by=user,
    )
    payment.status = CustomerPayment.Status.VOID
    payment.voided_at = timezone.now()
    payment.voided_by = user
    payment.void_reason = str(reason).strip()
    payment.save(update_fields=["status", "voided_at", "voided_by", "void_reason", "updated_at"])
    void_system_journal(shop=payment.shop, source_type="CUSTOMER_PAYMENT", source_id=payment.pk, user=user, reason=str(reason).strip())
    return payment
