from decimal import Decimal

from django.core.exceptions import ValidationError
from django.db import transaction
from django.utils import timezone

from shop_management.models.data.account_transaction import AccountTransaction, AccountTransactionType
from shop_management.models.data.purchase import Purchase
from shop_management.models.data.supplier_payment import SupplierPayment
from shop_management.services.automatic_journals import create_system_journal, financial_ledger_code, void_system_journal
from shop_management.services.account_controls import ensure_debit_allowed


def _validate(payment):
    errors = {}
    if payment.supplier.shop_id != payment.shop_id:
        errors["supplier"] = "Supplier 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:
        errors["amount"] = "Amount must be greater than zero."
    if payment.exchange_rate_used <= 0:
        errors["exchange_rate_used"] = "Exchange rate must be greater than zero."
    if payment.service_charge < 0:
        errors["service_charge"] = "Service charge cannot be negative."
    if payment.service_charge > 0 and not str(payment.notes or "").strip():
        errors["notes"] = "Explain the bank/Sarafi fee before posting this payment."
    allocated = Decimal("0")
    for row in payment.allocations.select_related("purchase"):
        if row.allocated_amount <= 0:
            errors[f"allocation_{row.pk}"] = "Allocated amount must be greater than zero."
        if row.purchase.document_status != Purchase.DocumentStatus.POSTED:
            errors[f"allocation_{row.pk}"] = "Only posted purchases may be paid."
        if row.purchase.shop_id != payment.shop_id or row.purchase.supplier_id != payment.supplier_id:
            errors[f"allocation_{row.pk}"] = "Purchase must belong to the same branch and supplier."
        if row.purchase.currency_id != payment.currency_id:
            errors[f"allocation_{row.pk}"] = "Purchase currency must match payment currency."
        legacy_paid = sum((p.amount for p in row.purchase.payments.all()), Decimal("0"))
        other_allocated = sum((a.allocated_amount for a in row.purchase.supplier_payment_allocations.filter(payment__status=SupplierPayment.Status.POSTED).exclude(payment=payment)), Decimal("0"))
        due = sum((Decimal(str(v)) for v in row.purchase.total_price.values()), Decimal("0")) - legacy_paid - other_allocated
        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 post_supplier_payment(payment_id, *, user):
    payment = SupplierPayment.objects.select_for_update().select_related("supplier", "shop", "financial_account__currency", "currency").get(pk=payment_id)
    if payment.status != SupplierPayment.Status.DRAFT:
        raise ValidationError("Only draft supplier payments can be posted.")
    _validate(payment)
    payment.base_amount = AccountTransaction.compute_converted_amount(payment.amount, payment.exchange_rate_used)
    total_account_deduction = AccountTransaction.compute_converted_amount(
        payment.amount + payment.service_charge, payment.exchange_rate_used
    )
    ensure_debit_allowed(payment.financial_account, total_account_deduction, label="supplier payment and fee")
    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=payment.payment_date,
        description=f"Supplier payment #{payment.pk} | {payment.reference}",
        reference=payment.reference, source_type="SUPPLIER_PAYMENT", source_id=str(payment.pk),
        is_system_generated=True, created_by=user,
    )
    if payment.service_charge:
        AccountTransaction.objects.create(
            account=payment.financial_account, account_currency=payment.financial_account.currency,
            transaction_currency=payment.currency, transaction_type=AccountTransactionType.DEBIT,
            amount=payment.service_charge, exchange_rate=payment.exchange_rate_used,
            converted_amount=AccountTransaction.compute_converted_amount(payment.service_charge, payment.exchange_rate_used),
            transaction_date=payment.payment_date,
            description=f"Supplier payment service charge #{payment.pk} | {payment.reference}",
            reference=payment.reference, source_type="SUPPLIER_PAYMENT_FEE", source_id=str(payment.pk),
            is_system_generated=True, created_by=user,
        )
    payment.status = SupplierPayment.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="SUPPLIER_PAYMENT", source_id=payment.pk,
        description=f"Supplier payment {payment.reference or payment.pk}", currency=payment.currency, exchange_rate=payment.exchange_rate_used, user=user,
        lines=[
            {"code": "2100", "side": "debit", "amount": payment.amount, "supplier": payment.supplier},
            {"code": "6500", "side": "debit", "amount": payment.service_charge},
            {"code": financial_ledger_code(payment.financial_account), "side": "credit", "amount": payment.amount + payment.service_charge, "financial_account": payment.financial_account, "supplier": payment.supplier},
        ])
    return payment


@transaction.atomic
def void_supplier_payment(payment_id, *, user, reason):
    payment = SupplierPayment.objects.select_for_update().select_related("financial_account__currency", "currency").get(pk=payment_id)
    if payment.status != SupplierPayment.Status.POSTED:
        raise ValidationError("Only posted supplier payments can be cancelled.")
    if not str(reason or "").strip():
        raise ValidationError({"void_reason": "A cancellation reason is required."})
    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=timezone.localdate(),
        description=f"Supplier payment void #{payment.pk} | {str(reason).strip()}",
        reference=payment.reference, source_type="SUPPLIER_PAYMENT_VOID", source_id=str(payment.pk),
        is_system_generated=True, created_by=user,
    )
    if payment.service_charge:
        AccountTransaction.objects.create(
            account=payment.financial_account, account_currency=payment.financial_account.currency,
            transaction_currency=payment.currency, transaction_type=AccountTransactionType.CREDIT,
            amount=payment.service_charge, exchange_rate=payment.exchange_rate_used,
            converted_amount=AccountTransaction.compute_converted_amount(payment.service_charge, payment.exchange_rate_used),
            transaction_date=timezone.localdate(),
            description=f"Supplier payment service charge void #{payment.pk} | {str(reason).strip()}",
            reference=payment.reference, source_type="SUPPLIER_PAYMENT_FEE_VOID", source_id=str(payment.pk),
            is_system_generated=True, created_by=user,
        )
    payment.status = SupplierPayment.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="SUPPLIER_PAYMENT", source_id=payment.pk, user=user, reason=str(reason).strip())
    return payment
