"""
Payment made to a supplier against their general balance (e.g. opening balance),
not against a specific purchase. Reduces the amount we owe the supplier.
"""
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.supplier import Supplier
from shop_management.models.data.shop_account import Account
from shop_management.models.data.utils import Currency


class SupplierBalancePayment(DataRoot):
    """Payment to supplier against balance (opening/general), not tied to a purchase."""
    supplier = models.ForeignKey(
        Supplier,
        on_delete=models.CASCADE,
        related_name="balance_payments",
    )
    account = models.ForeignKey(
        Account,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name="supplier_balance_payments",
    )
    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="supplier_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="supplier_balance_payment_settlements",
        help_text="Currency of the supplier 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 with supplier 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="supplier_balance_payments_voided",
    )
    void_reason = models.TextField(blank=True, default="")

    class Meta:
        ordering = ["-payment_date"]

    def __str__(self):
        curr = self.currency.title if self.currency else ""
        return f"Balance payment {self.amount} {curr} to {self.supplier.name}"

    @transaction.atomic
    def void(self, user=None, reason=""):
        from shop_management.models.data.account_transaction import (
            AccountTransaction,
            AccountTransactionType,
        )
        from shop_management.models.data.purchase import Purchase
        from shop_management.models.data.utils import classify_currency_code
        from shop_management.models.data.balance_payment_integrity import (
            resolve_balance_payment_transactions,
        )

        payment = SupplierBalancePayment.objects.select_for_update().get(pk=self.pk)
        if payment.is_void:
            raise ValidationError("This balance payment has already been cancelled.")

        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 SupplierBalancePaymentAllocation.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 == SupplierBalancePaymentAllocation.TargetType.OPENING_BALANCE:
                    calc_opening_afn += amt
            elif code == "USD":
                calc_usd += amt
                if alloc.target_type == SupplierBalancePaymentAllocation.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})."
            )

        supplier = Supplier.objects.select_for_update().get(pk=payment.supplier_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_purchases = {}  # purchase_id -> {"purchase": purchase_instance, "amount": Decimal}

        for alloc in allocations:
            target_type = alloc.target_type
            amt = Decimal(str(alloc.allocated_amount or 0))

            if target_type == SupplierBalancePaymentAllocation.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 == SupplierBalancePaymentAllocation.TargetType.PURCHASE:
                if not alloc.target_id:
                    raise ValidationError(f"Allocation #{alloc.id} missing target_id for PURCHASE.")
                if alloc.target_id not in req_purchases:
                    purchase = Purchase.objects.select_for_update().filter(pk=alloc.target_id).first()
                    if not purchase:
                        raise ValidationError(f"Purchase #{alloc.target_id} referenced in allocation does not exist.")
                    if purchase.supplier_id != supplier.id or purchase.shop_id != supplier.shop_id:
                        raise ValidationError(f"Purchase #{purchase.id} does not belong to supplier {supplier.name} or shop.")
                    if purchase.currency_id != alloc.target_currency_id:
                        raise ValidationError(f"Purchase #{purchase.id} currency does not match allocation currency.")
                    req_purchases[alloc.target_id] = {"purchase": purchase, "amount": Decimal("0")}
                req_purchases[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(supplier.opening_balance_afn_paid or 0))
        if current_afn_paid < req_afn_opening:
            raise ValidationError(
                f"Cannot cancel payment #{payment.id}: supplier AFN opening balance paid amount ({current_afn_paid}) "
                f"is less than allocation amount ({req_afn_opening}) to reverse."
            )

        current_usd_paid = Decimal(str(supplier.opening_balance_usd_paid or 0))
        if current_usd_paid < req_usd_opening:
            raise ValidationError(
                f"Cannot cancel payment #{payment.id}: supplier USD opening balance paid amount ({current_usd_paid}) "
                f"is less than allocation amount ({req_usd_opening}) to reverse."
            )

        for p_id, p_info in req_purchases.items():
            purchase = p_info["purchase"]
            amt_needed = p_info["amount"]
            cur_received = Decimal(str(purchase.received_amount or 0))
            if cur_received < amt_needed:
                raise ValidationError(
                    f"Cannot cancel payment #{payment.id}: purchase #{purchase.id} received/paid amount ({cur_received}) "
                    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=supplier.shop_id,
                source_type="SUPPLIER_BALANCE_PAYMENT",
                payment_side_type=AccountTransactionType.DEBIT,
                clearing_label="Purchase",
                description_prefix=f"Supplier balance payment #{payment.id} to {payment.supplier.name}",
            )

        # All checks passed! Now apply atomic updates
        supp_updates = set()
        if req_afn_opening > Decimal("0"):
            supplier.opening_balance_afn_paid = current_afn_paid - req_afn_opening
            supp_updates.add("opening_balance_afn_paid")
            code_legacy = classify_currency_code(supplier.opening_balance_currency)
            if code_legacy == "AFN" or not supplier.opening_balance_currency_id:
                supplier.opening_balance = Decimal(str(supplier.opening_balance or 0)) + req_afn_opening
                supp_updates.add("opening_balance")

        if req_usd_opening > Decimal("0"):
            supplier.opening_balance_usd_paid = current_usd_paid - req_usd_opening
            supp_updates.add("opening_balance_usd_paid")
            code_legacy = classify_currency_code(supplier.opening_balance_currency)
            if code_legacy == "USD" or not supplier.opening_balance_currency_id:
                supplier.opening_balance = Decimal(str(supplier.opening_balance or 0)) + req_usd_opening
                supp_updates.add("opening_balance")

        if supp_updates:
            supplier.save(update_fields=list(supp_updates))

        for p_id, p_info in req_purchases.items():
            purchase = p_info["purchase"]
            purchase.received_amount = Decimal(str(purchase.received_amount or 0)) - p_info["amount"]
            purchase.save(update_fields=["received_amount"])
            purchase.update_status()

        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 SupplierBalancePaymentAllocation(DataRoot):
    class TargetType(models.TextChoices):
        OPENING_BALANCE = "OPENING_BALANCE", "Opening Balance"
        PURCHASE = "PURCHASE", "Purchase"

    payment = models.ForeignKey(
        SupplierBalancePayment,
        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}"
