"""
Apply customer balance payments that were recorded but never reduced sale balances.

Root cause (fixed in serializer): Collect Payment used apply_to_opening_balance=True,
which previously applied only to opening/debtors and skipped unpaid sales.

Usage on server:
  python manage.py fix_unapplied_customer_payments --dry-run
  python manage.py fix_unapplied_customer_payments
  python manage.py fix_unapplied_customer_payments --customer-id=46
"""

from decimal import Decimal

from django.core.management.base import BaseCommand
from django.db import transaction

from shop_management.models.data.customer import Customer
from shop_management.models.data.customer_balance_payment import CustomerBalancePayment
from shop_management.models.data.customer_debtor import CustomerDebtor
from shop_management.models.data.sale import Sale
from shop_management.serializers.data.customer_balance_payment import (
    CustomerBalancePaymentSerializer,
)


class Command(BaseCommand):
    help = "Apply unapplied balance-payment gaps onto unpaid sales for all (or one) customers."

    def add_arguments(self, parser):
        parser.add_argument("--customer-id", type=int, help="Only this customer id")
        parser.add_argument("--customer-name", type=str, help="Filter by customer name contains")
        parser.add_argument("--shop-name", type=str, help="Filter by shop name contains")
        parser.add_argument("--dry-run", action="store_true")

    def handle(self, *args, **options):
        dry_run = options["dry_run"]
        qs = Customer.objects.select_related("shop").order_by("id")
        if options.get("customer_id"):
            qs = qs.filter(id=options["customer_id"])
        if options.get("customer_name"):
            qs = qs.filter(name__icontains=options["customer_name"].strip())
        if options.get("shop_name"):
            qs = qs.filter(shop__name__icontains=options["shop_name"].strip())

        fixed = 0
        skipped = 0
        total_applied = Decimal("0")

        for customer in qs:
            payments = list(
                CustomerBalancePayment.objects.filter(customer=customer).select_related("currency")
            )
            sales = list(
                Sale.objects.filter(customer=customer).select_related("currency")
            )
            if not payments:
                skipped += 1
                continue

            paid_from_payments = {}
            for bp in payments:
                if not bp.currency_id:
                    continue
                paid_from_payments[bp.currency_id] = paid_from_payments.get(
                    bp.currency_id, Decimal("0")
                ) + Decimal(str(bp.amount or 0)) + Decimal(str(getattr(bp, "discount", 0) or 0))

            paid_on_sales = {}
            for sale in sales:
                if not sale.currency_id:
                    continue
                total_paid = sale.total_paid or {}
                key = sale.currency.title if sale.currency else None
                amount = Decimal(str(total_paid.get(key, 0) or 0))
                paid_on_sales[sale.currency_id] = paid_on_sales.get(
                    sale.currency_id, Decimal("0")
                ) + amount

            settled_on_debtors = {}
            for loan in CustomerDebtor.objects.filter(customer=customer):
                if not loan.currency_id:
                    continue
                settled_on_debtors[loan.currency_id] = settled_on_debtors.get(
                    loan.currency_id, Decimal("0")
                ) + Decimal(str(loan.settled_amount or 0))

            # Skip customers who still have opening dues — gap math can confuse
            # opening collect-payments with unapplied sale payments.
            if (customer.opening_balance or 0) > 0 and not options.get("customer_id"):
                skipped += 1
                continue

            customer_applied = Decimal("0")
            with transaction.atomic():
                for currency_id, from_payments in paid_from_payments.items():
                    on_sales = paid_on_sales.get(currency_id, Decimal("0"))
                    on_debtors = settled_on_debtors.get(currency_id, Decimal("0"))
                    # Payments already reflected on sales or debtor settlements
                    # should not be applied again.
                    gap = from_payments - on_sales - on_debtors
                    if gap <= 0:
                        continue

                    class _FakePayment:
                        pass

                    fake = _FakePayment()
                    fake.customer_id = customer.id
                    fake.currency_id = currency_id
                    fake.amount = gap
                    fake.discount = Decimal("0")
                    fake.customer = customer

                    if dry_run:
                        remaining = gap
                        for sale in Sale.objects.filter(
                            customer_id=customer.id, currency_id=currency_id
                        ).order_by("sale_date", "id"):
                            if remaining <= 0:
                                break
                            remained_map = sale.total_remained_amount or {}
                            currency_key = sale.currency.title if sale.currency else None
                            sale_remaining = Decimal(
                                str(remained_map.get(currency_key, 0) or 0)
                            )
                            if sale_remaining <= 0:
                                continue
                            apply_amount = min(sale_remaining, remaining)
                            remaining -= apply_amount
                        applied = gap - max(remaining, Decimal("0"))
                    else:
                        leftover = CustomerBalancePaymentSerializer._apply_payment_to_sales(
                            fake
                        )
                        applied = gap - Decimal(str(leftover or 0))

                    if applied <= 0:
                        continue

                    customer_applied += applied
                    cur_title = next(
                        (
                            bp.currency.title
                            for bp in payments
                            if bp.currency_id == currency_id and bp.currency
                        ),
                        str(currency_id),
                    )
                    self.stdout.write(
                        f"Customer #{customer.id} {customer.name!r} "
                        f"({customer.shop.name if customer.shop else '—'}): "
                        f"apply {applied} {cur_title} (gap {gap})"
                    )

                if dry_run:
                    transaction.set_rollback(True)

            if customer_applied > 0:
                fixed += 1
                total_applied += customer_applied
            else:
                skipped += 1

        self.stdout.write(
            self.style.SUCCESS(
                f"Done. fixed={fixed} skipped={skipped} applied_total={total_applied} dry_run={dry_run}"
            )
        )
