from collections import defaultdict
from datetime import date
from decimal import Decimal

from django.db.models import Sum
from rest_framework.exceptions import ValidationError
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

from account.permissions import HasTabPermission, get_user_shop_filter
from shop_management.models.data.customer_payment import CustomerPayment
from shop_management.models.data.purchase import Purchase
from shop_management.models.data.purchase_return import PurchaseReturn
from shop_management.models.data.sale import Sale
from shop_management.models.data.sale_return import SaleReturn
from shop_management.models.data.shop import Shop
from shop_management.models.data.supplier_payment import SupplierPayment


BUCKETS = ("Current", "1-30", "31-60", "61-90", "90+")


def _bucket(due_date, as_of):
    overdue = (as_of - due_date).days
    if overdue <= 0: return "Current"
    if overdue <= 30: return "1-30"
    if overdue <= 60: return "31-60"
    if overdue <= 90: return "61-90"
    return "90+"


class AgingReportView(APIView):
    permission_classes = [IsAuthenticated, HasTabPermission]
    required_tab = "reports"

    def get(self, request):
        report_type = request.query_params.get("type", "receivable")
        if report_type not in ("receivable", "payable"):
            raise ValidationError({"type": "Type must be receivable or payable."})
        try:
            as_of = date.fromisoformat(request.query_params.get("as_of") or str(date.today()))
        except ValueError:
            raise ValidationError({"as_of": "Use YYYY-MM-DD format."})
        shop_id = request.query_params.get("shop")
        if not shop_id:
            raise ValidationError({"shop": "Branch is required."})
        try:
            shop = Shop.objects.get(get_user_shop_filter(request.user), pk=shop_id)
        except Shop.DoesNotExist:
            raise ValidationError({"shop": "Branch was not found or is not accessible."})
        rows = self._receivables(shop, as_of) if report_type == "receivable" else self._payables(shop, as_of)
        summary = defaultdict(lambda: {bucket: Decimal("0") for bucket in BUCKETS})
        for row in rows:
            summary[row["currency"]][row["bucket"]] += row["outstanding"]
        return Response({
            "type": report_type, "shop": shop.name, "as_of": as_of,
            "buckets": BUCKETS,
            "summary": [{"currency": currency, **{bucket: str(values[bucket].quantize(Decimal('0.01'))) for bucket in BUCKETS}, "total": str(sum(values.values(), Decimal('0')).quantize(Decimal('0.01')))} for currency, values in sorted(summary.items())],
            "rows": [{**row, "invoice_amount": str(row["invoice_amount"].quantize(Decimal("0.01"))), "paid_amount": str(row["paid_amount"].quantize(Decimal("0.01"))), "return_amount": str(row["return_amount"].quantize(Decimal("0.01"))), "outstanding": str(row["outstanding"].quantize(Decimal("0.01")))} for row in rows],
        })

    @staticmethod
    def _receivables(shop, as_of):
        result = []
        sales = Sale.objects.filter(shop=shop, uses_inventory_engine=True, document_status=Sale.DocumentStatus.POSTED, invoice_date__lte=as_of).select_related("customer", "currency")
        for sale in sales:
            gross = Decimal(sale.calculated_total or 0)
            legacy_paid = Decimal(sale.received_amount or 0) + (sale.payments.filter(payment_date__date__lte=as_of).aggregate(total=Sum("amount"))["total"] or Decimal("0"))
            allocated = sale.customer_payment_allocations.filter(payment__status=CustomerPayment.Status.POSTED, payment__payment_date__lte=as_of).aggregate(total=Sum("allocated_amount"))["total"] or Decimal("0")
            returned = sale.sale_returns.filter(document_status=SaleReturn.DocumentStatus.POSTED, return_date__date__lte=as_of).aggregate(total=Sum("total_refound_amount"))["total"] or Decimal("0")
            outstanding = max(gross - legacy_paid - allocated - returned, Decimal("0"))
            if not outstanding: continue
            due = sale.due_date or sale.invoice_date
            result.append({"document_id": sale.id, "document_number": sale.sale_number or f"Sale #{sale.id}", "party": sale.customer.name if sale.customer else sale.general_customer_name or "Walk-in customer", "invoice_date": sale.invoice_date, "due_date": due, "days_overdue": max((as_of - due).days, 0), "bucket": _bucket(due, as_of), "currency": sale.currency.title, "invoice_amount": gross, "paid_amount": legacy_paid + allocated, "return_amount": returned, "outstanding": outstanding})
        return sorted(result, key=lambda row: (row["due_date"], row["document_number"]))

    @staticmethod
    def _payables(shop, as_of):
        result = []
        purchases = Purchase.objects.filter(shop=shop, uses_inventory_engine=True, document_status=Purchase.DocumentStatus.POSTED, purchase_date__lte=as_of, is_deleted=False).select_related("supplier", "currency").prefetch_related("purchase_items")
        for purchase in purchases:
            gross = sum((Decimal(str(value)) for value in purchase.total_price.values()), Decimal("0"))
            legacy_paid = Decimal(purchase.received_amount or 0) + (purchase.payments.filter(payment_date__date__lte=as_of).aggregate(total=Sum("amount"))["total"] or Decimal("0"))
            allocated = purchase.supplier_payment_allocations.filter(payment__status=SupplierPayment.Status.POSTED, payment__payment_date__lte=as_of).aggregate(total=Sum("allocated_amount"))["total"] or Decimal("0")
            returned = PurchaseReturn.objects.filter(purchase=purchase, document_status=PurchaseReturn.DocumentStatus.POSTED, return_date__date__lte=as_of).aggregate(total=Sum("return_items__refund_amount"))["total"] or Decimal("0")
            outstanding = max(gross - legacy_paid - allocated - returned, Decimal("0"))
            if not outstanding: continue
            due = purchase.due_date or purchase.purchase_date
            result.append({"document_id": purchase.id, "document_number": purchase.purchase_number or f"Purchase #{purchase.id}", "party": purchase.supplier.name if purchase.supplier else "Unknown supplier", "invoice_date": purchase.purchase_date, "due_date": due, "days_overdue": max((as_of - due).days, 0), "bucket": _bucket(due, as_of), "currency": purchase.currency.title, "invoice_amount": gross, "paid_amount": legacy_paid + allocated, "return_amount": returned, "outstanding": outstanding})
        return sorted(result, key=lambda row: (row["due_date"], row["document_number"]))
