from decimal import Decimal
from django.db import models
from shop_management.models.base import DataRoot
from shop_management.models.data.shop import Shop
from shop_management.models.data.utils import (
    Currency,
    classify_currency_code,
    resolve_shop_currency,
    resolve_shop_currency_title,
)


class Customer(DataRoot):
    shop = models.ForeignKey(Shop, on_delete=models.CASCADE, related_name='customers')
    name = models.CharField(max_length=255, blank=True, null=True)
    email = models.EmailField(blank=True, null=True)
    contact = models.CharField(max_length=50, blank=True, null=True)
    address = models.CharField(max_length=255, blank=True, null=True)
    is_general_customer = models.BooleanField(default=False)
    # Legacy single-currency opening balance fields (kept for backward compatibility)
    opening_balance = models.DecimalField(
        max_digits=14, decimal_places=2, default=Decimal('0'), blank=True
    )
    opening_balance_currency = models.ForeignKey(
        Currency,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='customers_opening_balance',
    )
    # Dual-currency original opening balances & tracked payments against opening balances
    opening_balance_afn = models.DecimalField(
        max_digits=14, decimal_places=2, default=Decimal('0'), blank=True
    )
    opening_balance_usd = models.DecimalField(
        max_digits=14, decimal_places=2, default=Decimal('0'), blank=True
    )
    opening_balance_afn_paid = models.DecimalField(
        max_digits=14, decimal_places=2, default=Decimal('0'), blank=True
    )
    opening_balance_usd_paid = models.DecimalField(
        max_digits=14, decimal_places=2, default=Decimal('0'), blank=True
    )

    def __str__(self):
        return self.name or "Unnamed Customer"

    def _afn_currency_title(self):
        if (
            self.opening_balance_currency_id
            and classify_currency_code(self.opening_balance_currency) == "AFN"
        ):
            return self.opening_balance_currency.title
        return resolve_shop_currency_title(self.shop_id, "AFN")

    def _usd_currency_title(self):
        if (
            self.opening_balance_currency_id
            and classify_currency_code(self.opening_balance_currency) == "USD"
        ):
            return self.opening_balance_currency.title
        return resolve_shop_currency_title(self.shop_id, "USD")

    @property
    def remaining_opening_balance_afn(self):
        orig = Decimal(str(self.opening_balance_afn or 0))
        paid = Decimal(str(self.opening_balance_afn_paid or 0))
        if (
            orig == 0
            and paid == 0
            and Decimal(str(self.opening_balance or 0)) > 0
            and self.opening_balance_currency_id
            and classify_currency_code(self.opening_balance_currency) == "AFN"
        ):
            return Decimal(str(self.opening_balance or 0))
        return max(Decimal("0"), orig - paid)

    @property
    def remaining_opening_balance_usd(self):
        orig = Decimal(str(self.opening_balance_usd or 0))
        paid = Decimal(str(self.opening_balance_usd_paid or 0))
        if (
            orig == 0
            and paid == 0
            and Decimal(str(self.opening_balance or 0)) > 0
            and self.opening_balance_currency_id
            and classify_currency_code(self.opening_balance_currency) == "USD"
        ):
            return Decimal(str(self.opening_balance or 0))
        return max(Decimal("0"), orig - paid)

    def save(self, *args, **kwargs):
        update_fields = kwargs.get("update_fields")
        update_set = set(update_fields) if update_fields is not None else None

        orig_afn = Decimal(str(self.opening_balance_afn or 0))
        orig_usd = Decimal(str(self.opening_balance_usd or 0))
        paid_afn = Decimal(str(self.opening_balance_afn_paid or 0))
        paid_usd = Decimal(str(self.opening_balance_usd_paid or 0))
        legacy_ob = Decimal(str(self.opening_balance or 0))

        # 1) Legacy caller setting opening_balance + opening_balance_currency
        if (
            orig_afn == 0
            and orig_usd == 0
            and paid_afn == 0
            and paid_usd == 0
            and legacy_ob > 0
            and (
                update_set is None
                or (
                    "opening_balance" in update_set
                    and "opening_balance_afn" not in update_set
                    and "opening_balance_usd" not in update_set
                    and "opening_balance_afn_paid" not in update_set
                    and "opening_balance_usd_paid" not in update_set
                )
            )
        ):
            code = classify_currency_code(self.opening_balance_currency)
            if code == "USD":
                self.opening_balance_usd = legacy_ob
                if update_set is not None:
                    update_set.add("opening_balance_usd")
            elif code == "AFN" or not self.opening_balance_currency_id:
                self.opening_balance_afn = legacy_ob
                if update_set is not None:
                    update_set.add("opening_balance_afn")

        # 2) When full save or explicit edit of opening_balance_afn/usd, clamp paid & sync legacy mirror
        if update_set is None or "opening_balance_afn" in update_set or "opening_balance_usd" in update_set:
            if (
                update_set is None
                or (
                    "opening_balance_afn_paid" not in update_set
                    and "opening_balance_usd_paid" not in update_set
                )
            ):
                new_afn = max(Decimal("0"), Decimal(str(self.opening_balance_afn or 0)))
                new_usd = max(Decimal("0"), Decimal(str(self.opening_balance_usd or 0)))
                self.opening_balance_afn = new_afn
                self.opening_balance_usd = new_usd
                if paid_afn > new_afn:
                    self.opening_balance_afn_paid = new_afn
                    if update_set is not None:
                        update_set.add("opening_balance_afn_paid")
                if paid_usd > new_usd:
                    self.opening_balance_usd_paid = new_usd
                    if update_set is not None:
                        update_set.add("opening_balance_usd_paid")

        # Keep legacy opening_balance synced when dual-currency fields are used
        rem_afn = self.remaining_opening_balance_afn
        rem_usd = self.remaining_opening_balance_usd
        if update_set is None and (self.opening_balance_afn or self.opening_balance_usd or paid_afn or paid_usd):
            if rem_afn > 0:
                self.opening_balance = rem_afn
                afn_curr = resolve_shop_currency(self.shop_id, "AFN")
                if afn_curr:
                    self.opening_balance_currency = afn_curr
            elif rem_usd > 0:
                self.opening_balance = rem_usd
                usd_curr = resolve_shop_currency(self.shop_id, "USD")
                if usd_curr:
                    self.opening_balance_currency = usd_curr
            elif classify_currency_code(self.opening_balance_currency) in ("AFN", "USD", None):
                self.opening_balance = Decimal("0")

        if update_set is not None:
            kwargs["update_fields"] = list(update_set)
        return super().save(*args, **kwargs)

    def get_original_opening_balances_by_currency(self):
        result = {}
        orig_afn = Decimal(str(self.opening_balance_afn or 0))
        orig_usd = Decimal(str(self.opening_balance_usd or 0))
        if (
            orig_afn == 0
            and orig_usd == 0
            and Decimal(str(self.opening_balance or 0)) > 0
            and self.opening_balance_currency_id
        ):
            code = classify_currency_code(self.opening_balance_currency)
            if code == "AFN":
                orig_afn = Decimal(str(self.opening_balance))
            elif code == "USD":
                orig_usd = Decimal(str(self.opening_balance))
            else:
                result[self.opening_balance_currency.title] = float(self.opening_balance)
        if orig_afn > 0:
            result[self._afn_currency_title()] = float(orig_afn)
        if orig_usd > 0:
            result[self._usd_currency_title()] = float(orig_usd)
        return result

    def get_paid_opening_balances_by_currency(self):
        result = {}
        paid_afn = Decimal(str(self.opening_balance_afn_paid or 0))
        paid_usd = Decimal(str(self.opening_balance_usd_paid or 0))
        if paid_afn > 0:
            result[self._afn_currency_title()] = float(paid_afn)
        if paid_usd > 0:
            result[self._usd_currency_title()] = float(paid_usd)
        return result

    def get_opening_balances_by_currency(self):
        result = {}
        rem_afn = self.remaining_opening_balance_afn
        rem_usd = self.remaining_opening_balance_usd
        if rem_afn > 0:
            result[self._afn_currency_title()] = float(rem_afn)
        if rem_usd > 0:
            result[self._usd_currency_title()] = float(rem_usd)
        if (
            self.opening_balance_currency_id
            and classify_currency_code(self.opening_balance_currency) is None
            and Decimal(str(self.opening_balance or 0)) > 0
        ):
            result[self.opening_balance_currency.title] = float(self.opening_balance)
        return result

    def _add_opening_balances_to_map(self, result):
        rem_afn = self.remaining_opening_balance_afn
        rem_usd = self.remaining_opening_balance_usd
        if rem_afn > 0:
            key = self._afn_currency_title()
            result[key] = result.get(key, Decimal("0")) + rem_afn
        if rem_usd > 0:
            key = self._usd_currency_title()
            result[key] = result.get(key, Decimal("0")) + rem_usd
        if (
            self.opening_balance_currency_id
            and classify_currency_code(self.opening_balance_currency) is None
            and Decimal(str(self.opening_balance or 0)) > 0
        ):
            key = self.opening_balance_currency.title
            result[key] = result.get(key, Decimal("0")) + Decimal(str(self.opening_balance or 0))

    def get_total_owed_by_currency(self):
        """
        Total owed = remaining opening_balances + sale remainings after returns + outstanding debtor loans.
        Sale remaining = effective_total - sale_payments - return_amount (so returns reduce what they owe).
        Returns dict: { currency_title: total_owed_amount }.
        """
        result = {}
        self._add_opening_balances_to_map(result)
        for sale in self.sales.all():
            # New-engine drafts are only saved worksheets. They must not become
            # customer receivables until the sale is posted.
            if sale.uses_inventory_engine and sale.document_status != "POSTED":
                continue
            effective = sale.effective_total_amount or {}
            total_paid = sale.total_paid or {}
            total_return = sum(
                Decimal(str(r.total_refound_amount or 0))
                for r in sale.sale_returns.all()
                if not r.uses_inventory_engine or r.document_status == "POSTED"
            )
            for i, (currency_title, amount) in enumerate(effective.items()):
                amount = Decimal(str(amount))
                paid_val = Decimal(str(total_paid.get(currency_title, 0)))
                deduct_return = total_return if i == 0 else Decimal('0')
                contribution = amount - paid_val - deduct_return
                result[currency_title] = result.get(currency_title, Decimal('0')) + max(Decimal('0'), contribution)

        for loan in self.debtor_loans.all():
            if not loan.currency_id:
                continue
            key = loan.currency.title
            result[key] = result.get(key, Decimal("0")) + loan.outstanding_amount

        # Sale allocations are already included in Sale.total_paid. Any remainder
        # of a posted receipt reduces the customer's other debt in the same
        # currency; a negative result is exposed separately as customer credit.
        for payment in self.customer_payments.filter(status="POSTED").select_related("currency"):
            applied = payment.credit_applications.filter(
                sale__document_status="POSTED"
            ).aggregate(total=models.Sum("source_amount"))["total"] or Decimal("0")
            available = max(
                Decimal("0"),
                Decimal(str(payment.unallocated_amount or 0)) - applied,
            )
            if available <= 0:
                continue
            key = payment.currency.title
            result[key] = result.get(key, Decimal("0")) - available

        for bp in self.balance_payments.filter(is_void=False, unallocated_amount__gt=0).select_related("currency"):
            applied = bp.credit_applications.filter(
                sale__document_status="POSTED"
            ).aggregate(total=models.Sum("source_amount"))["total"] or Decimal("0")
            available = max(
                Decimal("0"),
                Decimal(str(bp.unallocated_amount or 0)) - applied,
            )
            if available <= 0:
                continue
            key = bp.currency.title if bp.currency else None
            if key:
                result[key] = result.get(key, Decimal("0")) - available

        # Remove negative balances and convert to float
        cleaned = {
            k: float(v) for k, v in result.items() if v > 0
        }
        return cleaned

    def get_credit_by_currency(self):
        """Posted receipt amounts available for this customer's future purchases."""
        result = {}

        # Rebuild the signed balance: the public owed method deliberately hides
        # negative balances, while this method shows their absolute value as credit.
        self._add_opening_balances_to_map(result)
        for sale in self.sales.all():
            if sale.uses_inventory_engine and sale.document_status != "POSTED":
                continue
            effective = sale.effective_total_amount or {}
            total_paid = sale.total_paid or {}
            total_return = sum(
                Decimal(str(r.total_refound_amount or 0))
                for r in sale.sale_returns.all()
                if not r.uses_inventory_engine or r.document_status == "POSTED"
            )
            for index, (currency_title, amount) in enumerate(effective.items()):
                contribution = Decimal(str(amount)) - Decimal(str(total_paid.get(currency_title, 0)))
                if index == 0:
                    contribution -= total_return
                result[currency_title] = result.get(currency_title, Decimal("0")) + max(Decimal("0"), contribution)
        for loan in self.debtor_loans.all():
            if loan.currency_id:
                key = loan.currency.title
                result[key] = result.get(key, Decimal("0")) + loan.outstanding_amount
        for payment in self.customer_payments.filter(status="POSTED").select_related("currency"):
            applied = payment.credit_applications.filter(
                sale__document_status="POSTED"
            ).aggregate(total=models.Sum("source_amount"))["total"] or Decimal("0")
            available = max(
                Decimal("0"),
                Decimal(str(payment.unallocated_amount or 0)) - applied,
            )
            if available > 0:
                key = payment.currency.title
                result[key] = result.get(key, Decimal("0")) - available

        for bp in self.balance_payments.filter(is_void=False, unallocated_amount__gt=0).select_related("currency"):
            applied = bp.credit_applications.filter(
                sale__document_status="POSTED"
            ).aggregate(total=models.Sum("source_amount"))["total"] or Decimal("0")
            available = max(
                Decimal("0"),
                Decimal(str(bp.unallocated_amount or 0)) - applied,
            )
            if available <= 0:
                continue
            key = bp.currency.title if bp.currency else None
            if key:
                result[key] = result.get(key, Decimal("0")) - available

        return {key: float(-amount) for key, amount in result.items() if amount < 0}
