"""
Payment collected from a customer against their general balance (e.g. opening balance),
not against a specific sale. Reduces the amount the customer owes.
"""
from decimal import Decimal
from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models, transaction
from django.utils import timezone
from shop_management.models.base import DataRoot
from shop_management.models.data.customer import Customer
from shop_management.models.data.shop import Shop
from shop_management.models.data.shop_account import Account
from shop_management.models.data.utils import Currency


class CustomerBalancePayment(DataRoot):
    """Payment from customer against balance (opening/general), not tied to a sale."""
    customer = models.ForeignKey(
        Customer,
        on_delete=models.CASCADE,
        related_name='balance_payments',
    )
    account = models.ForeignKey(
        Account,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="customer_balance_payments",
    )
    supplier_branch = models.ForeignKey(
        Shop,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="customer_balance_payments_as_supplier",
    )
    customer_branch = models.ForeignKey(
        Shop,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="customer_balance_payments_as_customer",
    )
    amount = models.DecimalField(max_digits=14, decimal_places=2)
    discount = models.DecimalField(max_digits=14, decimal_places=2, default=0)
    currency = models.ForeignKey(
        Currency,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='customer_balance_payments',
    )
    exchange_rate = models.DecimalField(max_digits=20, decimal_places=6, default=1)
    converted_amount = models.DecimalField(max_digits=18, decimal_places=2, default=0)
    settlement_currency = models.ForeignKey(
        Currency,
        on_delete=models.PROTECT,
        null=True,
        blank=True,
        related_name="customer_balance_payment_settlements",
        help_text="Currency of the customer debt reduced by this payment.",
    )
    settlement_exchange_rate = models.DecimalField(
        max_digits=20, decimal_places=6, default=1
    )
    settlement_amount = models.DecimalField(
        max_digits=18, decimal_places=2, default=0
    )
    secondary_exchange_rate = models.DecimalField(
        max_digits=20,
        decimal_places=6,
        null=True,
        blank=True,
        help_text="Exchange rate used when cascading payment surplus into secondary debt currency.",
    )
    allocated_to_afn = models.DecimalField(
        max_digits=18,
        decimal_places=2,
        default=Decimal("0"),
        help_text="Total AFN debt settled by this payment.",
    )
    allocated_to_usd = models.DecimalField(
        max_digits=18,
        decimal_places=2,
        default=Decimal("0"),
        help_text="Total USD debt settled by this payment.",
    )
    allocated_to_opening_afn = models.DecimalField(
        max_digits=18,
        decimal_places=2,
        default=Decimal("0"),
        help_text="Portion of this payment applied to AFN opening balance.",
    )
    allocated_to_opening_usd = models.DecimalField(
        max_digits=18,
        decimal_places=2,
        default=Decimal("0"),
        help_text="Portion of this payment applied to USD opening balance.",
    )
    unallocated_amount = models.DecimalField(
        max_digits=18,
        decimal_places=2,
        default=Decimal("0"),
        help_text="Overpayment or surplus credited to customer for future purchases.",
    )
    payment_date = models.DateTimeField(default=timezone.now)
    notes = models.TextField(blank=True, null=True)
    client_reference_id = models.CharField(
        max_length=100, blank=True, null=True, db_index=True
    )
    is_void = models.BooleanField(default=False, db_index=True)
    voided_at = models.DateTimeField(null=True, blank=True)
    voided_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        null=True,
        blank=True,
        on_delete=models.PROTECT,
        related_name="customer_balance_payments_voided",
    )
    void_reason = models.TextField(blank=True, default="")

    def __str__(self):
        curr = self.currency.title if self.currency else ""
        return f"Balance payment {self.amount} {curr} from {self.customer.name}"

    @transaction.atomic
    def void(self, user=None, reason=""):
        from shop_management.models.data.account_transaction import (
            AccountTransaction,
            AccountTransactionType,
        )
        from shop_management.models.data.customer_debtor import CustomerDebtor
        from shop_management.models.data.sale import Sale
        from shop_management.models.data.utils import classify_currency_code
        from shop_management.models.data.balance_payment_integrity import (
            resolve_balance_payment_transactions,
        )

        payment = CustomerBalancePayment.objects.select_for_update().get(pk=self.pk)
        if payment.is_void:
            raise ValidationError("This balance payment has already been cancelled.")

        # Guard: refuse to cancel while ANY active credit application is funded by this payment.
        # Active = linked to a sale that is not cancelled (draft and posted inventory-engine sales),
        # plus every legacy (non-inventory-engine) sale, which has no cancelled state.
        active_credit_apps = payment.credit_applications.select_for_update().filter(
            models.Q(sale__uses_inventory_engine=False)
            | ~models.Q(sale__document_status="VOID")
        )
        if active_credit_apps.exists():
            raise ValidationError(
                "Cannot cancel this payment because its credit has already been applied to a sale "
                "(draft or posted). Cancel the sale credit application first."
            )

        allocations = list(payment.allocations.select_for_update().all())
        has_debt_settled = (
            (payment.allocated_to_afn or Decimal("0")) > 0
            or (payment.allocated_to_usd or Decimal("0")) > 0
            or (payment.allocated_to_opening_afn or Decimal("0")) > 0
            or (payment.allocated_to_opening_usd or Decimal("0")) > 0
        )
        if has_debt_settled and not allocations:
            raise ValidationError(
                "This historical payment lacks detailed allocation records and cannot be safely cancelled."
            )

        # Integrity check 1: Ensure no allocation has already been reversed
        if any(alloc.is_reversed for alloc in allocations):
            raise ValidationError(
                f"Payment #{payment.id} has one or more allocations already marked as reversed. Cannot proceed with cancellation."
            )

        # Integrity check 2: Validate allocation ownership & sum totals
        calc_afn = Decimal("0")
        calc_usd = Decimal("0")
        calc_opening_afn = Decimal("0")
        calc_opening_usd = Decimal("0")

        for alloc in allocations:
            if alloc.payment_id != payment.id:
                raise ValidationError(f"Allocation #{alloc.id} does not belong to payment #{payment.id}.")
            amt = Decimal(str(alloc.allocated_amount or 0))
            if amt <= Decimal("0"):
                raise ValidationError(f"Allocation #{alloc.id} has invalid non-positive allocated amount ({amt}).")

            if alloc.target_type not in CustomerBalancePaymentAllocation.TargetType.values:
                raise ValidationError(
                    f"Allocation #{alloc.id} has unsupported target type '{alloc.target_type}'. "
                    "Cancellation rejected; no allocation was reversed."
                )

            code = classify_currency_code(alloc.target_currency)
            if code == "AFN":
                calc_afn += amt
                if alloc.target_type == CustomerBalancePaymentAllocation.TargetType.OPENING_BALANCE:
                    calc_opening_afn += amt
            elif code == "USD":
                calc_usd += amt
                if alloc.target_type == CustomerBalancePaymentAllocation.TargetType.OPENING_BALANCE:
                    calc_opening_usd += amt
            else:
                raise ValidationError(
                    f"Allocation #{alloc.id} has unsupported currency {alloc.target_currency}."
                )

        if (calc_afn - Decimal(str(payment.allocated_to_afn or 0))).copy_abs() > Decimal("0.01"):
            raise ValidationError(
                f"Payment #{payment.id} allocation total for AFN ({calc_afn}) does not match payment record ({payment.allocated_to_afn})."
            )
        if (calc_usd - Decimal(str(payment.allocated_to_usd or 0))).copy_abs() > Decimal("0.01"):
            raise ValidationError(
                f"Payment #{payment.id} allocation total for USD ({calc_usd}) does not match payment record ({payment.allocated_to_usd})."
            )
        if (calc_opening_afn - Decimal(str(payment.allocated_to_opening_afn or 0))).copy_abs() > Decimal("0.01"):
            raise ValidationError(
                f"Payment #{payment.id} allocation total for opening AFN ({calc_opening_afn}) does not match payment record ({payment.allocated_to_opening_afn})."
            )
        if (calc_opening_usd - Decimal(str(payment.allocated_to_opening_usd or 0))).copy_abs() > Decimal("0.01"):
            raise ValidationError(
                f"Payment #{payment.id} allocation total for opening USD ({calc_opening_usd}) does not match payment record ({payment.allocated_to_opening_usd})."
            )

        customer = Customer.objects.select_for_update().get(pk=payment.customer_id)

        # Integrity check 3: Aggregate required deductions and strictly validate target ownership, currency, and capacity
        req_afn_opening = Decimal("0")
        req_usd_opening = Decimal("0")
        req_sales = {}  # sale_id -> {"sale": sale_instance, "amount": Decimal}
        req_loans = {}  # loan_id -> {"loan": loan_instance, "amount": Decimal}

        for alloc in allocations:
            target_type = alloc.target_type
            amt = Decimal(str(alloc.allocated_amount or 0))

            if target_type == CustomerBalancePaymentAllocation.TargetType.OPENING_BALANCE:
                code = classify_currency_code(alloc.target_currency)
                if code == "AFN":
                    req_afn_opening += amt
                elif code == "USD":
                    req_usd_opening += amt

            elif target_type == CustomerBalancePaymentAllocation.TargetType.SALE:
                if not alloc.target_id:
                    raise ValidationError(f"Allocation #{alloc.id} missing target_id for SALE.")
                if alloc.target_id not in req_sales:
                    sale = Sale.objects.select_for_update().filter(pk=alloc.target_id).first()
                    if not sale:
                        raise ValidationError(f"Sale #{alloc.target_id} referenced in allocation does not exist.")
                    if sale.customer_id != customer.id or sale.shop_id != customer.shop_id:
                        raise ValidationError(f"Sale #{sale.id} does not belong to customer {customer.name} or shop.")
                    if sale.currency_id != alloc.target_currency_id:
                        raise ValidationError(f"Sale #{sale.id} currency does not match allocation currency.")
                    req_sales[alloc.target_id] = {"sale": sale, "amount": Decimal("0")}
                req_sales[alloc.target_id]["amount"] += amt

            elif target_type == CustomerBalancePaymentAllocation.TargetType.DEBTOR_LOAN:
                if not alloc.target_id:
                    raise ValidationError(f"Allocation #{alloc.id} missing target_id for DEBTOR_LOAN.")
                if alloc.target_id not in req_loans:
                    loan = CustomerDebtor.objects.select_for_update().filter(pk=alloc.target_id).first()
                    if not loan:
                        raise ValidationError(f"Debtor loan #{alloc.target_id} referenced in allocation does not exist.")
                    if loan.customer_id != customer.id:
                        raise ValidationError(f"Debtor loan #{loan.id} does not belong to customer {customer.name}.")
                    if loan.currency_id != alloc.target_currency_id:
                        raise ValidationError(f"Debtor loan #{loan.id} currency does not match allocation currency.")
                    req_loans[alloc.target_id] = {"loan": loan, "amount": Decimal("0")}
                req_loans[alloc.target_id]["amount"] += amt

            else:
                raise ValidationError(
                    f"Allocation #{alloc.id} has unsupported target type '{alloc.target_type}'. "
                    "Cancellation rejected; no allocation was reversed."
                )

        # Strict capacity validation: NEVER silently clamp with max(0, ...)
        current_afn_paid = Decimal(str(customer.opening_balance_afn_paid or 0))
        if current_afn_paid < req_afn_opening:
            raise ValidationError(
                f"Cannot cancel payment #{payment.id}: customer AFN opening balance paid amount ({current_afn_paid}) "
                f"is less than allocation amount ({req_afn_opening}) to reverse."
            )

        current_usd_paid = Decimal(str(customer.opening_balance_usd_paid or 0))
        if current_usd_paid < req_usd_opening:
            raise ValidationError(
                f"Cannot cancel payment #{payment.id}: customer USD opening balance paid amount ({current_usd_paid}) "
                f"is less than allocation amount ({req_usd_opening}) to reverse."
            )

        for s_id, s_info in req_sales.items():
            sale = s_info["sale"]
            amt_needed = s_info["amount"]
            cur_received = Decimal(str(sale.received_amount or 0))
            if cur_received < amt_needed:
                raise ValidationError(
                    f"Cannot cancel payment #{payment.id}: sale #{sale.id} received amount ({cur_received}) "
                    f"is less than allocation amount ({amt_needed}) to reverse."
                )

        for l_id, l_info in req_loans.items():
            loan = l_info["loan"]
            amt_needed = l_info["amount"]
            cur_settled = Decimal(str(loan.settled_amount or 0))
            if cur_settled < amt_needed:
                raise ValidationError(
                    f"Cannot cancel payment #{payment.id}: debtor loan #{loan.id} settled amount ({cur_settled}) "
                    f"is less than allocation amount ({amt_needed}) to reverse."
                )

        # Integrity check 4: strict AccountTransaction pair validation (no records modified here)
        txns_to_reverse = []
        if Decimal(str(payment.amount or 0)) > Decimal("0"):
            txns_to_reverse = resolve_balance_payment_transactions(
                payment,
                shop_id=customer.shop_id,
                source_type="CUSTOMER_BALANCE_PAYMENT",
                payment_side_type=AccountTransactionType.CREDIT,
                clearing_label="Sale",
                description_prefix=f"Customer balance payment #{payment.id} from {payment.customer.name}",
            )

        # All checks passed! Now apply atomic updates
        cust_updates = set()
        if req_afn_opening > Decimal("0"):
            customer.opening_balance_afn_paid = current_afn_paid - req_afn_opening
            cust_updates.add("opening_balance_afn_paid")
            code_legacy = classify_currency_code(customer.opening_balance_currency)
            if code_legacy == "AFN" or not customer.opening_balance_currency_id:
                customer.opening_balance = Decimal(str(customer.opening_balance or 0)) + req_afn_opening
                cust_updates.add("opening_balance")

        if req_usd_opening > Decimal("0"):
            customer.opening_balance_usd_paid = current_usd_paid - req_usd_opening
            cust_updates.add("opening_balance_usd_paid")
            code_legacy = classify_currency_code(customer.opening_balance_currency)
            if code_legacy == "USD" or not customer.opening_balance_currency_id:
                customer.opening_balance = Decimal(str(customer.opening_balance or 0)) + req_usd_opening
                cust_updates.add("opening_balance")

        if cust_updates:
            customer.save(update_fields=list(cust_updates))

        for s_id, s_info in req_sales.items():
            sale = s_info["sale"]
            sale.received_amount = Decimal(str(sale.received_amount or 0)) - s_info["amount"]
            sale.save(update_fields=["received_amount"])
            sale.update_status()

        for l_id, l_info in req_loans.items():
            loan = l_info["loan"]
            loan.settled_amount = Decimal(str(loan.settled_amount or 0)) - l_info["amount"]
            loan.save(update_fields=["settled_amount"])

        for alloc in allocations:
            alloc.is_reversed = True
            alloc.save(update_fields=["is_reversed"])

        for txn in txns_to_reverse:
            opposite = (
                AccountTransactionType.DEBIT
                if txn.transaction_type == AccountTransactionType.CREDIT
                else AccountTransactionType.CREDIT
            )
            AccountTransaction.objects.create(
                account=txn.account,
                account_currency=txn.account_currency,
                transaction_currency=txn.transaction_currency,
                transaction_type=opposite,
                amount=txn.amount,
                exchange_rate=txn.exchange_rate,
                converted_amount=txn.converted_amount,
                description=f"Reversal of payment #{payment.id}: {str(reason or 'Cancelled').strip()}",
                reference=txn.reference or str(payment.id),
                source_type="REVERSAL",
                source_id=str(txn.pk),
                is_system_generated=True,
                created_by=user,
                reversed_transaction=txn,
                transaction_date=timezone.localdate(),
            )
            txn.reversed_at = timezone.now()
            txn.reversed_by = user
            txn.reversal_reason = str(reason or "Cancelled").strip()
            txn.save(update_fields=["reversed_at", "reversed_by", "reversal_reason", "updated_at"])

        payment.is_void = True
        payment.voided_at = timezone.now()
        payment.voided_by = user
        payment.void_reason = str(reason or "").strip()
        payment.save(update_fields=["is_void", "voided_at", "voided_by", "void_reason"])

        self.is_void = payment.is_void
        self.voided_at = payment.voided_at
        self.voided_by = payment.voided_by
        self.void_reason = payment.void_reason
        return payment


class CustomerBalancePaymentAllocation(DataRoot):
    class TargetType(models.TextChoices):
        OPENING_BALANCE = "OPENING_BALANCE", "Opening Balance"
        SALE = "SALE", "Sale"
        DEBTOR_LOAN = "DEBTOR_LOAN", "Debtor Loan"

    payment = models.ForeignKey(
        CustomerBalancePayment,
        on_delete=models.CASCADE,
        related_name="allocations",
    )
    target_type = models.CharField(max_length=30, choices=TargetType.choices)
    target_id = models.PositiveIntegerField(null=True, blank=True)
    target_currency = models.ForeignKey(
        Currency,
        on_delete=models.PROTECT,
        related_name="+",
    )
    allocated_amount = models.DecimalField(max_digits=18, decimal_places=2)
    exchange_rate = models.DecimalField(
        max_digits=20, decimal_places=6, default=Decimal("1")
    )
    is_reversed = models.BooleanField(default=False)

    class Meta:
        ordering = ["id"]

    def __str__(self):
        curr = self.target_currency.title if self.target_currency else ""
        return f"Allocation {self.allocated_amount} {curr} to {self.target_type} #{self.target_id}"
