# views.py
from rest_framework import viewsets, filters, status
from rest_framework.decorators import action
from rest_framework.response import Response
from rest_framework.views import APIView
from django.core.exceptions import FieldError
from rest_framework.permissions import IsAuthenticated
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Count, Q, Sum, Max
from decimal import Decimal
from django.utils import timezone

from .models import (
    FeeCategory,
    FeeComponent,
    ClassFeeStructure,
    StudentFeeAssignment,
    FeePayment,
    PaymentFeeComponent,
    PaymentAllocation,
    OutstandingBalance,
    TransportFeePayment,
)

from academics.models import (
    StudentEnrollment,
)

from people.models import (
    Student, Parent, StudentParent
)

from transport.models import StudentTransport

from .serializers import (
    FeeCategorySerializer,
    FeeComponentSerializer,
    ClassFeeStructureSerializer,
    StudentFeeAssignmentSerializer,
)


class FeeCategoryViewSet(viewsets.ModelViewSet):
    """
    ViewSet for FeeCategory CRUD operations
    """

    queryset = FeeCategory.objects.all()
    serializer_class = FeeCategorySerializer
    permission_classes = [IsAuthenticated]
    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter,
    ]
    filterset_fields = ["is_active"]
    search_fields = ["name", "description"]
    ordering_fields = ["name", "created_at", "updated_at"]
    ordering = ["name"]

    def get_queryset(self):
        queryset = super().get_queryset()
        # Annotate with fee components count
        queryset = queryset.annotate(fee_components_count=Count("fee_components"))
        return queryset

    def perform_create(self, serializer):
        serializer.save()

    def perform_update(self, serializer):
        serializer.save()

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        # Check if category has fee components
        if instance.fee_components.exists():
            return Response(
                {"error": "Cannot delete category with associated fee components"},
                status=status.HTTP_400_BAD_REQUEST,
            )
        return super().destroy(request, *args, **kwargs)

    @action(detail=True, methods=["get"])
    def fee_components(self, request, pk=None):
        """
        Get all fee components under this category
        """
        category = self.get_object()
        components = category.fee_components.filter(is_active=True)
        serializer = FeeComponentSerializer(components, many=True)
        return Response(serializer.data)


class FeeComponentViewSet(viewsets.ModelViewSet):
    """
    ViewSet for FeeComponent CRUD operations
    """

    queryset = FeeComponent.objects.all()
    serializer_class = FeeComponentSerializer
    permission_classes = [IsAuthenticated]
    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter,
    ]
    filterset_fields = [
        "category",
        "is_mandatory",
        "is_recurring",
        "is_active",
        "calculation_type",
    ]
    search_fields = ["name", "code", "description"]
    ordering_fields = ["name", "code", "created_at", "updated_at"]
    ordering = ["category__name", "name"]

    def get_permissions(self):
        """
        Set permissions based on action
        """
        permission_classes = [IsAuthenticated]

        if self.action in ["create"]:
            permission_classes = [IsAuthenticated]
            self.permission_required = "fees.add_feecomponent"
        elif self.action in ["update", "partial_update"]:
            permission_classes = [IsAuthenticated]
            self.permission_required = "fees.change_feecomponent"
        elif self.action in ["destroy"]:
            permission_classes = [IsAuthenticated]
            self.permission_required = "fees.delete_feecomponent"

        return [perm() for perm in permission_classes]

    def get_queryset(self):
        queryset = super().get_queryset()

        # Filter by category if provided
        category_id = self.request.query_params.get("category")
        if category_id:
            queryset = queryset.filter(category_id=category_id)

        return queryset.select_related("category")

    def perform_create(self, serializer):
        serializer.save()

    def perform_update(self, serializer):
        serializer.save()

    def destroy(self, request, *args, **kwargs):
        instance = self.get_object()
        # Check if component is used in any fee structure
        if instance.class_fee_structures.exists():
            return Response(
                {"error": "Cannot delete fee component that is assigned to classes"},
                status=status.HTTP_400_BAD_REQUEST,
            )
        return super().destroy(request, *args, **kwargs)


class ClassFeeStructureViewSet(viewsets.ModelViewSet):
    """
    ViewSet for ClassFeeStructure CRUD operations
    """

    queryset = ClassFeeStructure.objects.all()
    serializer_class = ClassFeeStructureSerializer
    permission_classes = [IsAuthenticated]
    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter,
    ]
    filterset_fields = ["academic_class", "fee_component", "academic_term", "is_active"]
    search_fields = [
        "notes",
        "fee_component__name",
        "academic_class__standard__name",
        "academic_class__section__name",
    ]
    ordering_fields = ["amount", "due_date", "created_at"]
    ordering = ["academic_class", "fee_component"]

    def get_queryset(self):
        queryset = super().get_queryset()
        return queryset.select_related(
            "academic_class", "fee_component", "academic_term"
        )

    def perform_create(self, serializer):
        serializer.save()

    @action(detail=True, methods=["get"])
    def student_assignments(self, request, pk=None):
        """
        Get all student assignments for this fee structure
        """
        fee_structure = self.get_object()
        assignments = fee_structure.student_assignments.filter(is_active=True)
        serializer = StudentFeeAssignmentSerializer(assignments, many=True)
        return Response(serializer.data)


class StudentFeeAssignmentViewSet(viewsets.ModelViewSet):
    """
    ViewSet for StudentFeeAssignment CRUD operations
    """

    queryset = StudentFeeAssignment.objects.all()
    serializer_class = StudentFeeAssignmentSerializer
    permission_classes = [IsAuthenticated]
    filter_backends = [
        DjangoFilterBackend,
        filters.SearchFilter,
        filters.OrderingFilter,
    ]
    filterset_fields = [
        "student",
        "enrollment",
        "class_fee_structure",
        "is_active",
        "discount_type",
    ]
    search_fields = ["reason", "student__full_name", "student__roll_number"]
    ordering_fields = ["created_at", "due_date_override"]
    ordering = ["-created_at"]

    def get_queryset(self):
        queryset = super().get_queryset()

        # Optimize with select_related to include all related data
        queryset = queryset.select_related(
            "student",
            "enrollment",
            "enrollment__academic_class",
            "enrollment__academic_class__academic_year",  # Academic year through class
            "enrollment__academic_class__standard",
            "enrollment__academic_class__section",
            "class_fee_structure",
            "class_fee_structure__fee_component",
            "class_fee_structure__academic_term",
        )

        # Filter by academic year (through enrollment -> academic_class -> academic_year)
        academic_year = self.request.query_params.get("academic_year")
        if academic_year:
            queryset = queryset.filter(
                enrollment__academic_class__academic_year_id=academic_year
            )

        # Filter by class through enrollment
        class_id = self.request.query_params.get("class")
        if class_id:
            queryset = queryset.filter(enrollment__academic_class_id=class_id)

        # Alternative parameter for class
        class_param = self.request.query_params.get("class_id")
        if class_param and not class_id:
            queryset = queryset.filter(enrollment__academic_class_id=class_param)

        # Filter by class through fee structure (if needed)
        class_fs_id = self.request.query_params.get(
            "class_fee_structure__academic_class"
        )
        if class_fs_id:
            queryset = queryset.filter(
                class_fee_structure__academic_class_id=class_fs_id
            )

        # Filter by student if provided
        student_id = self.request.query_params.get("student")
        if student_id:
            queryset = queryset.filter(student_id=student_id)

        # Filter by enrollment if provided
        enrollment_id = self.request.query_params.get("enrollment")
        if enrollment_id:
            queryset = queryset.filter(enrollment_id=enrollment_id)

        # Filter by academic term (through class_fee_structure)
        term_id = self.request.query_params.get("academic_term")
        if term_id:
            queryset = queryset.filter(class_fee_structure__academic_term_id=term_id)

        return queryset

    def perform_create(self, serializer):
        serializer.save()

    @action(detail=False, methods=["get"])
    def student_fees_summary(self, request):
        """
        Get fee summary for a specific student
        """
        student_id = request.query_params.get("student_id")
        if not student_id:
            return Response(
                {"error": "student_id is required"}, status=status.HTTP_400_BAD_REQUEST
            )

        assignments = self.get_queryset().filter(student_id=student_id, is_active=True)

        total_fees = sum(
            assignment.calculate_final_amount() for assignment in assignments
        )
        total_discount = sum(
            assignment.class_fee_structure.amount - assignment.calculate_final_amount()
            for assignment in assignments
        )

        return Response(
            {
                "student_id": student_id,
                "total_fees": total_fees,
                "total_discount": total_discount,
                "number_of_fees": assignments.count(),
                "assignments": StudentFeeAssignmentSerializer(
                    assignments, many=True
                ).data,
            }
        )


class MarkAsPaidView(APIView):
    """
    Mark all outstanding fee balance for an enrollment as paid.
    Creates a single FeePayment record with status='completed' for the remaining balance.
    """
    permission_classes = [IsAuthenticated]

    def post(self, request):
        enrollment_id = request.data.get('enrollment_id')
        payment_method = request.data.get('payment_method', 'cash')
        notes = request.data.get('notes', '')

        if not enrollment_id:
            return Response({'error': 'enrollment_id is required'}, status=status.HTTP_400_BAD_REQUEST)

        try:
            enrollment = StudentEnrollment.objects.select_related('student', 'academic_class').get(id=enrollment_id)
        except StudentEnrollment.DoesNotExist:
            return Response({'error': 'Enrollment not found'}, status=status.HTTP_404_NOT_FOUND)

        fee_structures = list(ClassFeeStructure.objects.filter(
            academic_class_id=enrollment.academic_class_id, is_active=True
        ).select_related('fee_component', 'academic_term'))
        discount_assignments = StudentFeeAssignment.objects.filter(
            enrollment_id=enrollment_id, is_active=True
        ).select_related('class_fee_structure')
        assignment_map = {a.class_fee_structure_id: a for a in discount_assignments}

        # Per-component already-paid amounts (via PaymentFeeComponent)
        comp_paid_agg = (
            PaymentFeeComponent.objects.filter(
                payment__student_enrollment_id=enrollment_id,
                payment__status='completed',
            )
            .values('class_fee_structure_id')
            .annotate(paid=Sum('amount_paid'))
        )
        comp_paid_map = {r['class_fee_structure_id']: r['paid'] for r in comp_paid_agg}

        # Build per-component balance list
        component_balances = []
        total_fee = Decimal('0')
        for fs in fee_structures:
            assign = assignment_map.get(fs.id)
            fee_amt = Decimal(str(assign.calculate_final_amount())) if assign else fs.amount
            already_paid = comp_paid_map.get(fs.id, Decimal('0'))
            if not isinstance(already_paid, Decimal):
                already_paid = Decimal(str(already_paid))
            comp_balance = fee_amt - already_paid
            if comp_balance > 0:
                component_balances.append((fs, assign, comp_balance))
            total_fee += fee_amt

        # Transport fee (from the student's active StudentTransport assignment)
        # is tracked separately via TransportFeePayment, not PaymentFeeComponent.
        transport = StudentTransport.objects.filter(
            enrollment_id=enrollment_id, is_active=True
        ).first()
        transport_balance = Decimal('0')
        if transport:
            transport_fee_amt = Decimal(str(transport.fee_amount or 0))
            transport_already_paid = TransportFeePayment.objects.filter(
                student_transport=transport
            ).aggregate(total=Sum('amount_paid'))['total'] or Decimal('0')
            if not isinstance(transport_already_paid, Decimal):
                transport_already_paid = Decimal(str(transport_already_paid))
            transport_balance = max(Decimal('0'), transport_fee_amt - transport_already_paid)
            total_fee += transport_fee_amt

        paid_agg = FeePayment.objects.filter(
            student_enrollment_id=enrollment_id, status='completed'
        ).aggregate(total=Sum('amount_paid'))['total'] or Decimal('0')
        total_paid = paid_agg if isinstance(paid_agg, Decimal) else Decimal(str(paid_agg))
        transport_paid_agg = TransportFeePayment.objects.filter(
            student_transport__enrollment_id=enrollment_id
        ).aggregate(total=Sum('amount_paid'))['total'] or Decimal('0')
        if not isinstance(transport_paid_agg, Decimal):
            transport_paid_agg = Decimal(str(transport_paid_agg))
        total_paid += transport_paid_agg

        balance = total_fee - total_paid
        if balance <= 0:
            return Response({'error': 'Fee is already fully paid'}, status=status.HTTP_400_BAD_REQUEST)

        fee_component_balance = balance - transport_balance
        receipt_number = None
        if fee_component_balance > 0:
            payment = FeePayment(
                student_enrollment=enrollment,
                student=enrollment.student,
                amount_paid=fee_component_balance,
                payment_date=timezone.now().date(),
                payment_method=payment_method,
                status='completed',
                payment_type='full',
                principal_amount=fee_component_balance,
                late_fee_amount=Decimal('0'),
                balance_before=fee_component_balance,
                balance_after=Decimal('0'),
                notes=notes or 'Marked as paid manually',
            )
            payment.save()
            receipt_number = payment.receipt_number

            # Create PaymentFeeComponent for each component so component-level status is consistent
            for fs, assign, comp_balance in component_balances:
                PaymentFeeComponent.objects.create(
                    payment=payment,
                    fee_component=fs.fee_component,
                    amount_paid=comp_balance,
                    class_fee_structure=fs,
                    student_fee_assignment=assign,
                    academic_term=fs.academic_term,
                )

        if transport_balance > 0:
            TransportFeePayment.objects.create(
                student_transport=transport,
                amount_paid=transport_balance,
                payment_date=timezone.now().date(),
                payment_method=payment_method,
                notes=notes or 'Marked as paid manually',
            )

        return Response(
            {'message': 'Fee marked as paid', 'receipt_number': receipt_number},
            status=status.HTTP_200_OK,
        )


class MarkAsUnpaidView(APIView):
    """
    Remove all completed payment records for an enrollment, effectively marking it as unpaid.
    """
    permission_classes = [IsAuthenticated]

    def post(self, request):
        enrollment_id = request.data.get('enrollment_id')

        if not enrollment_id:
            return Response({'error': 'enrollment_id is required'}, status=status.HTTP_400_BAD_REQUEST)

        deleted_count, _ = FeePayment.objects.filter(
            student_enrollment_id=enrollment_id, status='completed'
        ).delete()

        transport_deleted_count, _ = TransportFeePayment.objects.filter(
            student_transport__enrollment_id=enrollment_id
        ).delete()
        deleted_count += transport_deleted_count

        return Response(
            {'message': f'{deleted_count} payment(s) removed', 'deleted_count': deleted_count},
            status=status.HTTP_200_OK,
        )


class MarkComponentPaidView(APIView):
    """
    Mark a single fee component as paid for an enrollment.
    Creates a FeePayment + PaymentFeeComponent for the component's remaining balance.
    """
    permission_classes = [IsAuthenticated]

    def post(self, request):
        enrollment_id = request.data.get('enrollment_id')
        class_fee_structure_id = request.data.get('class_fee_structure_id')
        payment_method = request.data.get('payment_method', 'cash')

        if not enrollment_id or not class_fee_structure_id:
            return Response(
                {'error': 'enrollment_id and class_fee_structure_id are required'},
                status=status.HTTP_400_BAD_REQUEST,
            )

        try:
            enrollment = StudentEnrollment.objects.select_related('student').get(id=enrollment_id)
        except StudentEnrollment.DoesNotExist:
            return Response({'error': 'Enrollment not found'}, status=status.HTTP_404_NOT_FOUND)

        try:
            fee_structure = ClassFeeStructure.objects.select_related(
                'fee_component', 'academic_term'
            ).get(id=class_fee_structure_id, is_active=True)
        except ClassFeeStructure.DoesNotExist:
            return Response({'error': 'Fee structure not found'}, status=status.HTTP_404_NOT_FOUND)

        discount_assign = StudentFeeAssignment.objects.filter(
            enrollment_id=enrollment_id,
            class_fee_structure=fee_structure,
            is_active=True,
        ).first()
        fee_amt = (
            Decimal(str(discount_assign.calculate_final_amount()))
            if discount_assign
            else fee_structure.amount
        )

        comp_paid_agg = (
            PaymentFeeComponent.objects.filter(
                class_fee_structure_id=class_fee_structure_id,
                payment__student_enrollment_id=enrollment_id,
                payment__status='completed',
            ).aggregate(total=Sum('amount_paid'))['total'] or Decimal('0')
        )
        comp_paid = comp_paid_agg if isinstance(comp_paid_agg, Decimal) else Decimal(str(comp_paid_agg))

        balance = fee_amt - comp_paid
        if balance <= 0:
            return Response(
                {'error': 'This fee component is already fully paid'},
                status=status.HTTP_400_BAD_REQUEST,
            )

        payment = FeePayment(
            student_enrollment=enrollment,
            student=enrollment.student,
            amount_paid=balance,
            payment_date=timezone.now().date(),
            payment_method=payment_method,
            status='completed',
            payment_type='component',
            principal_amount=balance,
            late_fee_amount=Decimal('0'),
            balance_before=balance,
            balance_after=Decimal('0'),
            notes=f'Component "{fee_structure.fee_component.name}" marked as paid manually',
        )
        payment.save()

        PaymentFeeComponent.objects.create(
            payment=payment,
            fee_component=fee_structure.fee_component,
            amount_paid=balance,
            class_fee_structure=fee_structure,
            student_fee_assignment=discount_assign,
            academic_term=fee_structure.academic_term,
        )

        return Response(
            {'message': 'Component marked as paid', 'receipt_number': payment.receipt_number},
            status=status.HTTP_200_OK,
        )


class MarkComponentUnpaidView(APIView):
    """
    Remove all completed payment records for a specific fee component of an enrollment.
    Deletes PaymentFeeComponent rows and any parent FeePayments that become empty.
    """
    permission_classes = [IsAuthenticated]

    def post(self, request):
        enrollment_id = request.data.get('enrollment_id')
        class_fee_structure_id = request.data.get('class_fee_structure_id')

        if not enrollment_id or not class_fee_structure_id:
            return Response(
                {'error': 'enrollment_id and class_fee_structure_id are required'},
                status=status.HTTP_400_BAD_REQUEST,
            )

        pfc_qs = PaymentFeeComponent.objects.filter(
            class_fee_structure_id=class_fee_structure_id,
            payment__student_enrollment_id=enrollment_id,
            payment__status='completed',
        ).select_related('payment')

        payment_ids = list(pfc_qs.values_list('payment_id', flat=True).distinct())
        deleted_count = pfc_qs.count()
        pfc_qs.delete()

        # Delete any FeePayments that now have no PaymentFeeComponent rows left
        for payment_id in payment_ids:
            try:
                payment = FeePayment.objects.get(id=payment_id)
                if not payment.payment_components.exists():
                    payment.delete()
            except FeePayment.DoesNotExist:
                pass

        return Response(
            {'message': f'{deleted_count} component payment(s) removed', 'deleted_count': deleted_count},
            status=status.HTTP_200_OK,
        )


class MarkTransportFeePaidView(APIView):
    """
    Mark a student's transport fee (from their active StudentTransport
    assignment) as paid. Creates a TransportFeePayment for the remaining
    balance — mirrors MarkComponentPaidView but for the transport component.
    """
    permission_classes = [IsAuthenticated]

    def post(self, request):
        enrollment_id = request.data.get('enrollment_id')

        if not enrollment_id:
            return Response({'error': 'enrollment_id is required'}, status=status.HTTP_400_BAD_REQUEST)

        transport = StudentTransport.objects.filter(
            enrollment_id=enrollment_id, is_active=True
        ).first()
        if not transport:
            return Response({'error': 'No active transport assignment found'}, status=status.HTTP_404_NOT_FOUND)

        fee_amt = Decimal(str(transport.fee_amount or 0))
        already_paid = TransportFeePayment.objects.filter(
            student_transport=transport
        ).aggregate(total=Sum('amount_paid'))['total'] or Decimal('0')
        if not isinstance(already_paid, Decimal):
            already_paid = Decimal(str(already_paid))

        balance = fee_amt - already_paid
        if balance <= 0:
            return Response(
                {'error': 'Transport fee is already fully paid'},
                status=status.HTTP_400_BAD_REQUEST,
            )

        payment = TransportFeePayment.objects.create(
            student_transport=transport,
            amount_paid=balance,
            payment_date=timezone.now().date(),
            payment_method=request.data.get('payment_method', 'cash'),
            notes='Transport fee marked as paid manually',
        )

        return Response(
            {'message': 'Transport fee marked as paid', 'payment_id': payment.id},
            status=status.HTTP_200_OK,
        )


class MarkTransportFeeUnpaidView(APIView):
    """
    Remove all TransportFeePayment records for a student's active transport
    assignment, marking the transport fee as unpaid again.
    """
    permission_classes = [IsAuthenticated]

    def post(self, request):
        enrollment_id = request.data.get('enrollment_id')

        if not enrollment_id:
            return Response({'error': 'enrollment_id is required'}, status=status.HTTP_400_BAD_REQUEST)

        deleted_count, _ = TransportFeePayment.objects.filter(
            student_transport__enrollment_id=enrollment_id
        ).delete()

        return Response(
            {'message': f'{deleted_count} transport payment(s) removed', 'deleted_count': deleted_count},
            status=status.HTTP_200_OK,
        )


class FeeCollectionView(APIView):
    """
    Fee Collections dashboard — all students' fee payment status with filters.
    Filters: academic_year, class_id, roll_number, student_name, payment_status
    """
    permission_classes = [IsAuthenticated]

    def get(self, request):

        academic_year_id = request.query_params.get("academic_year")
        class_id = request.query_params.get("class_id")
        roll_number = request.query_params.get("roll_number", "").strip()
        student_name = request.query_params.get("student_name", "").strip()
        payment_status_filter = request.query_params.get("payment_status", "").strip()
        due_date_from = request.query_params.get("due_date_from", "").strip()
        due_date_to = request.query_params.get("due_date_to", "").strip()
        overdue_only = request.query_params.get("overdue_only", "").strip().lower() in ("1", "true", "yes")
        page = max(1, int(request.query_params.get("page", 1)))
        page_size = max(1, min(100, int(request.query_params.get("page_size", 20))))

        def _parse_date(value):
            if not value:
                return None
            try:
                return timezone.datetime.strptime(value, "%Y-%m-%d").date()
            except ValueError:
                return None

        due_date_from = _parse_date(due_date_from)
        due_date_to = _parse_date(due_date_to)
        today = timezone.now().date()

        # Base enrollment queryset
        enrollments_qs = StudentEnrollment.objects.select_related(
            "student",
            "academic_class",
            "academic_class__academic_year",
            "academic_class__standard",
            "academic_class__section",
        ).filter(is_active=True)

        if academic_year_id:
            enrollments_qs = enrollments_qs.filter(
                academic_class__academic_year_id=academic_year_id
            )
        if class_id:
            enrollments_qs = enrollments_qs.filter(academic_class_id=class_id)
        if roll_number:
            enrollments_qs = enrollments_qs.filter(
                student__roll_number__icontains=roll_number
            )
        if student_name:
            enrollments_qs = enrollments_qs.filter(
                Q(student__first_name__icontains=student_name)
                | Q(student__last_name__icontains=student_name)
            )

        all_enrollments = list(enrollments_qs)
        enrollment_ids = [e.id for e in all_enrollments]
        class_ids = list({e.academic_class_id for e in all_enrollments})

        # Batch: ClassFeeStructures for all relevant classes
        fee_structures = ClassFeeStructure.objects.filter(
            academic_class_id__in=class_ids, is_active=True
        ).select_related("fee_component", "fee_component__category", "academic_term")
        class_fee_map = {}
        for fs in fee_structures:
            class_fee_map.setdefault(fs.academic_class_id, []).append(fs)

        # Batch: StudentFeeAssignments (discounts)
        discount_assignments = StudentFeeAssignment.objects.filter(
            enrollment_id__in=enrollment_ids, is_active=True
        ).select_related("class_fee_structure")
        assignment_map = {}
        for a in discount_assignments:
            assignment_map[(a.enrollment_id, a.class_fee_structure_id)] = a

        # Batch: Total paid per enrollment (completed payments only)
        paid_agg = (
            FeePayment.objects.filter(
                student_enrollment_id__in=enrollment_ids, status="completed"
            )
            .values("student_enrollment_id")
            .annotate(total_paid=Sum("amount_paid"))
        )
        paid_map = {r["student_enrollment_id"]: r["total_paid"] for r in paid_agg}

        # Batch: Paid per (enrollment, fee_component) via PaymentFeeComponent
        comp_paid_agg = (
            PaymentFeeComponent.objects.filter(
                payment__student_enrollment_id__in=enrollment_ids,
                payment__status="completed",
            )
            .values("payment__student_enrollment_id", "fee_component_id")
            .annotate(component_paid=Sum("amount_paid"))
        )
        comp_paid_map = {}
        for r in comp_paid_agg:
            key = (r["payment__student_enrollment_id"], r["fee_component_id"])
            comp_paid_map[key] = r["component_paid"]

        # Batch: Last payment date and count per enrollment
        last_payment_agg = (
            FeePayment.objects.filter(
                student_enrollment_id__in=enrollment_ids, status="completed"
            )
            .values("student_enrollment_id")
            .annotate(last_date=Max("payment_date"), count=Count("id"))
        )
        last_payment_map = {
            r["student_enrollment_id"]: {"last_date": r["last_date"], "count": r["count"]}
            for r in last_payment_agg
        }

        # Batch: each enrollment's active transport assignment (if any) and
        # how much of its fee has been paid so far via TransportFeePayment
        transport_map = {}
        for t in StudentTransport.objects.filter(
            enrollment_id__in=enrollment_ids, is_active=True
        ):
            transport_map.setdefault(t.enrollment_id, t)

        transport_paid_agg = (
            TransportFeePayment.objects.filter(
                student_transport__enrollment_id__in=enrollment_ids
            )
            .values("student_transport__enrollment_id")
            .annotate(total_paid=Sum("amount_paid"))
        )
        transport_paid_map = {
            r["student_transport__enrollment_id"]: r["total_paid"] for r in transport_paid_agg
        }

        # Build per-student results
        results = []
        for enrollment in all_enrollments:
            structures = class_fee_map.get(enrollment.academic_class_id, [])
            total_fee = Decimal("0")
            components = []

            next_due_date = None  # earliest due date among components still owing a balance

            for fs in structures:
                assign = assignment_map.get((enrollment.id, fs.id))
                fee_amt = Decimal(str(assign.calculate_final_amount())) if assign else fs.amount

                total_fee += fee_amt

                comp_paid = comp_paid_map.get(
                    (enrollment.id, fs.fee_component_id), Decimal("0")
                )
                if not isinstance(comp_paid, Decimal):
                    comp_paid = Decimal(str(comp_paid))
                comp_balance = max(Decimal("0"), fee_amt - comp_paid)

                if fee_amt == 0 or comp_paid >= fee_amt:
                    comp_status = "full"
                elif comp_paid > 0:
                    comp_status = "partial"
                else:
                    comp_status = "unpaid"

                # A per-student due date override (set on the StudentFeeAssignment)
                # takes precedence over the class-level fee structure's due date.
                effective_due_date = (
                    assign.due_date_override if assign and assign.due_date_override else fs.due_date
                )

                if comp_balance > 0 and effective_due_date:
                    if next_due_date is None or effective_due_date < next_due_date:
                        next_due_date = effective_due_date

                components.append({
                    "class_fee_structure_id": fs.id,
                    "component_id": fs.fee_component_id,
                    "component_name": fs.fee_component.name,
                    "component_code": fs.fee_component.code,
                    "category": fs.fee_component.category.name if fs.fee_component.category else None,
                    "academic_term_id": fs.academic_term_id,
                    "term": fs.academic_term.name if fs.academic_term else None,
                    "due_date": str(effective_due_date) if effective_due_date else None,
                    "amount": float(fee_amt),
                    "paid": float(comp_paid),
                    "balance": float(comp_balance),
                    "status": comp_status,
                    "has_discount": assign is not None,
                    "is_overdue": bool(comp_balance > 0 and effective_due_date and effective_due_date < today),
                })

            # Transport fee — surfaced as its own component, tracked via
            # TransportFeePayment rather than PaymentFeeComponent since it's
            # a flat per-assignment amount, not a class-level FeeComponent.
            transport_paid_raw = Decimal("0")
            transport = transport_map.get(enrollment.id)
            if transport:
                transport_fee_amt = Decimal(str(transport.fee_amount or 0))
                transport_paid_raw = transport_paid_map.get(enrollment.id, Decimal("0"))
                if not isinstance(transport_paid_raw, Decimal):
                    transport_paid_raw = Decimal(str(transport_paid_raw))
                transport_balance = max(Decimal("0"), transport_fee_amt - transport_paid_raw)

                if transport_fee_amt == 0 or transport_paid_raw >= transport_fee_amt:
                    transport_status = "full"
                elif transport_paid_raw > 0:
                    transport_status = "partial"
                else:
                    transport_status = "unpaid"

                transport_due = transport.due_date
                if transport_balance > 0 and transport_due:
                    if next_due_date is None or transport_due < next_due_date:
                        next_due_date = transport_due

                total_fee += transport_fee_amt

                components.append({
                    "class_fee_structure_id": None,
                    "component_id": None,
                    "component_type": "transport",
                    "transport_assignment_id": transport.id,
                    "component_name": "Transport Fee",
                    "component_code": "TRANSPORT",
                    "category": "Transport",
                    "academic_term_id": None,
                    "term": None,
                    "due_date": str(transport_due) if transport_due else None,
                    "amount": float(transport_fee_amt),
                    "paid": float(transport_paid_raw),
                    "balance": float(transport_balance),
                    "status": transport_status,
                    "has_discount": False,
                    "is_overdue": bool(transport_balance > 0 and transport_due and transport_due < today),
                })

            total_paid_raw = paid_map.get(enrollment.id, Decimal("0"))
            if not isinstance(total_paid_raw, Decimal):
                total_paid_raw = Decimal(str(total_paid_raw))
            total_paid_raw += transport_paid_raw
            balance = max(Decimal("0"), total_fee - total_paid_raw)

            if total_fee == 0:
                e_status = "unpaid"
            elif total_paid_raw >= total_fee:
                e_status = "full"
            elif total_paid_raw > 0:
                e_status = "partial"
            else:
                e_status = "unpaid"

            lp = last_payment_map.get(enrollment.id, {})
            student = enrollment.student
            ac = enrollment.academic_class

            results.append({
                "enrollment_id": enrollment.id,
                "student": {
                    "id": student.id,
                    "first_name": student.first_name or "",
                    "last_name": student.last_name or "",
                    "roll_number": student.roll_number or "",
                    "student_id": student.student_id or "",
                },
                "class": {
                    "id": ac.id,
                    "standard": ac.standard.name,
                    "section": ac.section.name,
                    "academic_year": ac.academic_year.name,
                },
                "total_fee": float(total_fee),
                "total_paid": float(total_paid_raw),
                "balance": float(balance),
                "payment_status": e_status,
                "last_payment_date": str(lp["last_date"]) if lp.get("last_date") else None,
                "payment_count": lp.get("count", 0),
                "next_due_date": str(next_due_date) if next_due_date else None,
                "is_overdue": bool(next_due_date and balance > 0 and next_due_date < today),
                "components": components,
            })

        # Filter by computed payment_status (in-memory)
        if payment_status_filter in ("full", "partial", "unpaid"):
            results = [r for r in results if r["payment_status"] == payment_status_filter]

        # Filter by next due date (in-memory) — students with no outstanding
        # due date (nothing owed) are excluded once a due-date filter is active
        if due_date_from:
            results = [
                r for r in results
                if r["next_due_date"] and r["next_due_date"] >= str(due_date_from)
            ]
        if due_date_to:
            results = [
                r for r in results
                if r["next_due_date"] and r["next_due_date"] <= str(due_date_to)
            ]
        if overdue_only:
            results = [r for r in results if r["is_overdue"]]

        # Summary
        total_students = len(results)
        total_fee_assigned = sum(r["total_fee"] for r in results)
        total_collected = sum(r["total_paid"] for r in results)
        total_balance_amt = sum(r["balance"] for r in results)
        fully_paid = sum(1 for r in results if r["payment_status"] == "full")
        partially_paid = sum(1 for r in results if r["payment_status"] == "partial")
        unpaid_count = sum(1 for r in results if r["payment_status"] == "unpaid")
        overdue_count = sum(1 for r in results if r["is_overdue"])

        # Pagination
        offset = (page - 1) * page_size
        paginated = results[offset: offset + page_size]

        return Response({
            "summary": {
                "total_students": total_students,
                "total_fee_assigned": round(total_fee_assigned, 2),
                "total_collected": round(total_collected, 2),
                "total_balance": round(total_balance_amt, 2),
                "fully_paid": fully_paid,
                "partially_paid": partially_paid,
                "unpaid": unpaid_count,
                "overdue": overdue_count,
            },
            "count": total_students,
            "page": page,
            "page_size": page_size,
            "results": paginated,
        })

