from django import forms
from django.conf import settings
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction
from django.db.models import Q, Count, Sum
from django.http import JsonResponse
from django.urls import reverse
from django.utils import timezone
from django.views import View
from django.views.generic import TemplateView, ListView

from accounts.mixins import StudentRequiredMixin, TeacherRequiredMixin, AdminRequiredMixin, SuperAdminRequiredMixin
from portal.permissions import ModulePermissionMixin, MODULES
from portal.models import AdminModulePermission
from admissions.models import Application, ApplicationDocument, Enrollment, Program
from admissions.forms import ApplicationReviewForm
from admissions.documents import DocumentUploadForm, save_application_document, document_checklist
from profiles.models import StudentProfile, TeacherProfile
from communications.models import Notification
from communications.utils import (
    notify_application_approved,
    notify_application_rejected,
    create_notification,
)
from website.models import Testimonial, GalleryImage
from website.forms import TestimonialForm
from courses.models import Course
from payments.models import Payment, Donation
from timetable.models import TimetableEntry
from certificates.models import Certificate
from content.views import _student_has_access
from portal.exports import excel_response

User = get_user_model()


# ---------------------------------------------------------------------------
# Shared helpers
# ---------------------------------------------------------------------------

def _mark_notifications_read(user):
    user.notifications.filter(is_read=False).update(is_read=True)


# ---------------------------------------------------------------------------
# Student views
# ---------------------------------------------------------------------------

class StudentDashboardView(StudentRequiredMixin, TemplateView):
    template_name = 'student/dashboard.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        user = self.request.user
        enrollment = getattr(user, 'enrollment', None)
        ctx['enrollment'] = enrollment
        ctx['application'] = getattr(user, 'application', None)
        ctx['unread_notifications'] = user.notifications.filter(is_read=False)
        ctx['recent_notifications'] = user.notifications.all()[:5]
        ctx['course_count'] = (
            Course.objects.filter(program=enrollment.program, is_active=True).count()
            if enrollment else 0
        )
        ctx['total_paid'] = (
            Payment.objects.filter(enrollment=enrollment).aggregate(total=Sum('amount'))['total'] or 0
            if enrollment else 0
        )
        ctx['content_locked'] = bool(enrollment and not _student_has_access(enrollment))
        return ctx


# ---------------------------------------------------------------------------
# Teacher views
# ---------------------------------------------------------------------------

import datetime as _dt


class TeacherDashboardView(TeacherRequiredMixin, TemplateView):
    template_name = 'teacher/dashboard.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        profile = getattr(self.request.user, 'teacher_profile', None)
        programs = profile.programs.filter(is_active=True) if profile else Program.objects.none()
        ctx['programs'] = programs
        ctx['program_count'] = programs.count()
        ctx['course_count'] = Course.objects.filter(program__in=programs, is_active=True).count()
        ctx['student_count'] = Enrollment.objects.filter(program__in=programs, is_active=True).count()
        today_iso = _dt.date.today().isoweekday()  # 1=Mon…7=Sun
        if today_iso <= 6:
            ctx['today_entries'] = list(
                TimetableEntry.objects.filter(program__in=programs, day=today_iso)
                .select_related('program', 'course').order_by('start_time')
            )
            ctx['today_name'] = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][today_iso - 1]
        else:
            ctx['today_entries'] = []
            ctx['today_name'] = 'Sunday'
        return ctx


# ---------------------------------------------------------------------------
# Admin views
# ---------------------------------------------------------------------------

class AdminDashboardView(AdminRequiredMixin, TemplateView):
    template_name = 'admin_portal/dashboard.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['total_applications'] = Application.objects.count()
        ctx['pending_count'] = Application.objects.filter(status=Application.PENDING).count()
        ctx['under_review_count'] = Application.objects.filter(status=Application.UNDER_REVIEW).count()
        ctx['approved_count'] = Application.objects.filter(status=Application.APPROVED).count()
        ctx['rejected_count'] = Application.objects.filter(status=Application.REJECTED).count()
        ctx['total_students'] = Enrollment.objects.filter(is_active=True).count()
        ctx['recent_applications'] = Application.objects.order_by('-submitted_at')[:8]
        ctx['program_breakdown'] = (
            Enrollment.objects.filter(is_active=True)
            .values('program__name')
            .annotate(count=Count('id'))
            .order_by('-count')
        )
        # Donations
        completed_donations = Donation.objects.filter(status=Donation.COMPLETED)
        ctx['total_donations'] = completed_donations.aggregate(total=Sum('amount'))['total'] or 0
        ctx['donations_count'] = completed_donations.count()
        # Ungraded assignment submissions
        from content.models import Submission
        ctx['ungraded_count'] = Submission.objects.filter(grade='').count()
        return ctx


class ApplicationsListView(AdminRequiredMixin, ModulePermissionMixin, ListView):
    module = 'applications'
    model = Application
    template_name = 'admin_portal/applications_list.html'
    context_object_name = 'applications'
    paginate_by = 20

    def get_queryset(self):
        qs = Application.objects.select_related('program_applied').order_by('-submitted_at')
        status = self.request.GET.get('status')
        search = self.request.GET.get('q')
        if status:
            qs = qs.filter(status=status)
        if search:
            qs = qs.filter(
                Q(first_name__icontains=search) |
                Q(last_name__icontains=search) |
                Q(email__icontains=search) |
                Q(phone_number__icontains=search)
            )
        return qs

    def get(self, request, *args, **kwargs):
        if request.GET.get('export') == '1':
            qs = self.get_queryset()
            headers = ['Ref No.', 'Full Name', 'Email', 'Phone', 'Program',
                       'Status', 'Fee Paid', 'Submitted Date']
            rows = [
                (
                    a.reference_number,
                    a.full_name,
                    a.email,
                    a.phone_number,
                    a.program_applied.name if a.program_applied else '',
                    a.get_status_display(),
                    'Yes' if a.application_fee_paid else 'No',
                    a.submitted_at.strftime('%d/%m/%Y') if a.submitted_at else '',
                )
                for a in qs
            ]
            return excel_response('Applications', 'MLS_Applications', headers, rows)
        return super().get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['current_status'] = self.request.GET.get('status', '')
        ctx['search_query'] = self.request.GET.get('q', '')
        ctx['status_choices'] = Application.STATUS_CHOICES
        return ctx


class ApplicationReviewView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'applications'
    template_name = 'admin_portal/application_review.html'

    def get(self, request, pk):
        application = get_object_or_404(Application, pk=pk)
        form = ApplicationReviewForm(instance=application)
        return render(request, self.template_name, {
            'application': application,
            'form': form,
            'documents': application.documents.all(),
        })

    def post(self, request, pk):
        application = get_object_or_404(Application, pk=pk)
        action = request.POST.get('action')

        if action == 'under_review':
            application.status = Application.UNDER_REVIEW
            application.save(update_fields=['status', 'updated_at'])
            messages.info(request, f'{application.full_name} marked as Under Review.')

        elif action == 'approve':
            if application.status == Application.APPROVED:
                messages.warning(request, 'This application is already approved.')
                return redirect('portal:application_review', pk=pk)
            self._approve(request, application)

        elif action == 'reject':
            rejection_reason = request.POST.get('rejection_reason', '').strip()
            if not rejection_reason:
                messages.error(request, 'Please provide a reason for rejection.')
                return redirect('portal:application_review', pk=pk)
            self._reject(request, application, rejection_reason)

        return redirect('portal:applications_list')

    @transaction.atomic
    def _approve(self, request, application):
        temp_password = User.objects.make_random_password(length=10)
        user = User.objects.create_user(
            username=application.email,
            email=application.email,
            first_name=application.first_name,
            last_name=application.last_name,
            phone=application.phone_number,
            role=User.STUDENT,
            password=temp_password,
            is_verified=True,
        )
        Enrollment.objects.create(
            student=user,
            application=application,
            program=application.program_applied,
        )
        StudentProfile.objects.create(user=user)
        application.status = Application.APPROVED
        application.applicant_user = user
        application.reviewed_by = request.user
        application.reviewed_at = timezone.now()
        application.save()
        create_notification(
            request.user,
            'Application Approved',
            f'Application for {application.full_name} ({application.program_applied}) approved successfully.',
        )
        notify_application_approved(user, application, temp_password)
        messages.success(
            request,
            f'Application approved. Account created for {application.full_name}. '
            f'Login credentials sent to {application.email}.'
        )

    def _reject(self, request, application, rejection_reason):
        application.status = Application.REJECTED
        application.rejection_reason = rejection_reason
        application.admin_notes = request.POST.get('admin_notes', '').strip()
        application.reviewed_by = request.user
        application.reviewed_at = timezone.now()
        application.save()
        notify_application_rejected(application)
        messages.warning(request, f'Application for {application.full_name} rejected.')


class ApplicationFeeToggleView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'applications'
    module_action = 'edit'
    """Admin manually marks an application fee as paid (e.g. cash walk-in)."""
    def post(self, request, pk):
        application = get_object_or_404(Application, pk=pk)
        if not application.application_fee_paid:
            application.application_fee_paid = True
            application.application_fee_paid_at = timezone.now()
            application.application_fee_txref = 'MANUAL'
            if application.status == Application.AWAITING_PAYMENT:
                application.status = Application.PENDING
            application.save(update_fields=[
                'application_fee_paid', 'application_fee_paid_at',
                'application_fee_txref', 'status', 'updated_at',
            ])
            messages.success(request, f'Application fee manually marked as paid for {application.full_name}.')
        else:
            messages.info(request, 'Fee was already marked as paid.')
        return redirect('portal:application_review', pk=pk)


class StudentListView(AdminRequiredMixin, ModulePermissionMixin, ListView):
    module = 'students'
    template_name = 'admin_portal/student_list.html'
    context_object_name = 'enrollments'
    paginate_by = 20

    def get_queryset(self):
        qs = Enrollment.objects.select_related('student', 'program').filter(is_active=True)
        search = self.request.GET.get('q')
        program = self.request.GET.get('program')
        if search:
            qs = qs.filter(
                Q(student__first_name__icontains=search) |
                Q(student__last_name__icontains=search) |
                Q(student__email__icontains=search) |
                Q(student_number__icontains=search)
            )
        if program:
            qs = qs.filter(program_id=program)
        return qs.order_by('-enrolled_at')

    def get(self, request, *args, **kwargs):
        if request.GET.get('export') == '1':
            qs = self.get_queryset()
            headers = ['Student No.', 'Full Name', 'Email', 'Phone',
                       'Program', 'Level', 'Enrolled Date']
            rows = [
                (
                    e.student_number,
                    e.student.get_full_name(),
                    e.student.email,
                    e.student.phone or '',
                    e.program.name,
                    e.program.get_level_display(),
                    e.enrolled_at.strftime('%d/%m/%Y') if e.enrolled_at else '',
                )
                for e in qs
            ]
            return excel_response('Students', 'MLS_Students', headers, rows)
        return super().get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['programs'] = Program.objects.filter(is_active=True)
        ctx['search_query'] = self.request.GET.get('q', '')
        ctx['current_program'] = self.request.GET.get('program', '')
        return ctx


class StudentDetailView(AdminRequiredMixin, ModulePermissionMixin, TemplateView):
    module = 'students'
    template_name = 'admin_portal/student_detail.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        enrollment = get_object_or_404(
            Enrollment.objects.select_related('student', 'program', 'application'),
            pk=self.kwargs['pk'],
        )
        ctx['enrollment'] = enrollment
        ctx['student'] = enrollment.student
        ctx['application'] = enrollment.application
        ctx['notifications'] = enrollment.student.notifications.all()[:10]
        payments = enrollment.payments.order_by('-payment_date')
        ctx['payments'] = payments
        ctx['payment_total'] = payments.aggregate(total=Sum('amount'))['total'] or 0
        ctx['certificates'] = enrollment.certificates.order_by('-issue_date')
        ctx['documents'] = document_checklist(enrollment.application) if enrollment.application else None
        return ctx


class StudentToggleEditsView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'students'
    module_action = 'edit'
    """Admin flips whether a student may edit their contact details and documents."""
    def post(self, request, pk):
        enrollment = get_object_or_404(Enrollment, pk=pk)
        enrollment.edits_unlocked = not enrollment.edits_unlocked
        enrollment.save(update_fields=['edits_unlocked'])
        state = 'unlocked' if enrollment.edits_unlocked else 'locked'
        messages.success(request, f'Profile editing {state} for {enrollment.student.get_full_name()}.')
        return redirect('portal:student_detail', pk=pk)


class StudentDocumentUploadView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'students'
    module_action = 'edit'
    """Admin uploads/replaces a student's supporting document (anytime)."""
    def post(self, request, pk):
        enrollment = get_object_or_404(Enrollment, pk=pk)
        if not enrollment.application:
            messages.error(request, 'This student has no linked application to attach documents to.')
            return redirect('portal:student_detail', pk=pk)
        form = DocumentUploadForm(request.POST, request.FILES)
        if form.is_valid():
            _, created = save_application_document(
                enrollment.application, form.cleaned_data['document_type'], form.cleaned_data['file']
            )
            label = dict(ApplicationDocument.DOCUMENT_TYPE_CHOICES)[form.cleaned_data['document_type']]
            messages.success(request, f'{label} {"uploaded" if created else "replaced"}.')
        else:
            first_error = next(iter(form.errors.values()))[0] if form.errors else 'Could not upload the file.'
            messages.error(request, first_error)
        return redirect('portal:student_detail', pk=pk)


class StudentContentAccessView(AdminRequiredMixin, ModulePermissionMixin, View):
    """Admin sets content_access_override on a student's enrollment."""
    module = 'students'
    module_action = 'edit'

    VALID_STATES = ('default', 'enabled', 'disabled')

    def post(self, request, pk):
        enrollment = get_object_or_404(Enrollment, pk=pk)
        state = request.POST.get('state', 'default')
        if state not in self.VALID_STATES:
            messages.error(request, 'Invalid content access state.')
            return redirect('portal:student_detail', pk=pk)
        enrollment.content_access_override = state
        enrollment.save(update_fields=['content_access_override'])
        labels = {'default': 'Default (payment threshold)', 'enabled': 'Force Enabled', 'disabled': 'Force Disabled'}
        messages.success(
            request,
            f'Content access for {enrollment.student.get_full_name()} set to: {labels[state]}.'
        )
        return redirect('portal:student_detail', pk=pk)


class MarkNotificationsReadView(LoginRequiredMixin, View):
    def post(self, request):
        _mark_notifications_read(request.user)
        return redirect(request.META.get('HTTP_REFERER', request.user.get_dashboard_url()))


class MarkOneNotificationReadView(LoginRequiredMixin, View):
    """Mark a single notification (belonging to the current user) as read."""
    def post(self, request, pk):
        notif = get_object_or_404(Notification, pk=pk, recipient=request.user)
        if not notif.is_read:
            notif.is_read = True
            notif.save(update_fields=['is_read'])
        return redirect(request.META.get('HTTP_REFERER', request.user.get_dashboard_url()))


# ---------------------------------------------------------------------------
# Testimonials (admin CRUD)
# ---------------------------------------------------------------------------

class TestimonialListView(AdminRequiredMixin, ModulePermissionMixin, ListView):
    module = 'testimonials'
    model = Testimonial
    template_name = 'admin_portal/testimonials/list.html'
    context_object_name = 'testimonials'
    paginate_by = 20
    ordering = ['display_order', '-created_at']


class TestimonialCreateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'testimonials'
    module_action = 'edit'
    template_name = 'admin_portal/testimonials/form.html'

    def get(self, request):
        return render(request, self.template_name, {'form': TestimonialForm(), 'action': 'Add'})

    def post(self, request):
        form = TestimonialForm(request.POST, request.FILES)
        if form.is_valid():
            form.save()
            messages.success(request, 'Testimonial added successfully.')
            return redirect('portal:testimonial_list')
        return render(request, self.template_name, {'form': form, 'action': 'Add'})


class TestimonialUpdateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'testimonials'
    module_action = 'edit'
    template_name = 'admin_portal/testimonials/form.html'

    def get(self, request, pk):
        testimonial = get_object_or_404(Testimonial, pk=pk)
        return render(request, self.template_name, {
            'form': TestimonialForm(instance=testimonial),
            'testimonial': testimonial,
            'action': 'Edit',
        })

    def post(self, request, pk):
        testimonial = get_object_or_404(Testimonial, pk=pk)
        form = TestimonialForm(request.POST, request.FILES, instance=testimonial)
        if form.is_valid():
            form.save()
            messages.success(request, 'Testimonial updated successfully.')
            return redirect('portal:testimonial_list')
        return render(request, self.template_name, {
            'form': form, 'testimonial': testimonial, 'action': 'Edit',
        })


class TestimonialDeleteView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'testimonials'
    module_action = 'delete'
    template_name = 'admin_portal/testimonials/confirm_delete.html'

    def get(self, request, pk):
        testimonial = get_object_or_404(Testimonial, pk=pk)
        return render(request, self.template_name, {'testimonial': testimonial})

    def post(self, request, pk):
        testimonial = get_object_or_404(Testimonial, pk=pk)
        name = testimonial.name
        testimonial.delete()
        messages.success(request, f'Testimonial by "{name}" deleted.')
        return redirect('portal:testimonial_list')


# ---------------------------------------------------------------------------
# Courses (admin CRUD + student view)
# ---------------------------------------------------------------------------

class _CourseForm(forms.ModelForm):
    class Meta:
        model = Course
        fields = ['program', 'name', 'code', 'description', 'year', 'is_active']
        widgets = {'description': forms.Textarea(attrs={'rows': 3})}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        from crispy_forms.helper import FormHelper
        from crispy_forms.layout import Layout, Row, Column, Submit, Field
        self.helper = FormHelper()
        self.helper.layout = Layout(
            'program',
            Row(
                Column('name', css_class='col-md-8'),
                Column('code', css_class='col-md-4'),
            ),
            'description',
            Row(
                Column('year', css_class='col-md-4'),
                Column('is_active', css_class='col-md-4'),
            ),
            Submit('submit', 'Save Course', css_class='btn btn-primary'),
        )


class CourseListView(AdminRequiredMixin, ModulePermissionMixin, ListView):
    module = 'courses'
    model = Course
    template_name = 'admin_portal/courses/list.html'
    context_object_name = 'courses'
    paginate_by = 30

    def get_queryset(self):
        qs = Course.objects.select_related('program').order_by('program__name', 'year', 'name')
        program_id = self.request.GET.get('program')
        q = self.request.GET.get('q', '').strip()
        if program_id:
            qs = qs.filter(program_id=program_id)
        if q:
            qs = qs.filter(
                Q(name__icontains=q) |
                Q(code__icontains=q) |
                Q(description__icontains=q)
            )
        return qs

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['programs'] = Program.objects.filter(is_active=True)
        ctx['current_program'] = self.request.GET.get('program', '')
        ctx['search_query'] = self.request.GET.get('q', '')
        return ctx


class CourseCreateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'courses'
    module_action = 'edit'
    template_name = 'admin_portal/courses/form.html'

    def get(self, request):
        form = _CourseForm(initial={'program': request.GET.get('program')})
        return render(request, self.template_name, {'form': form, 'action': 'Add'})

    def post(self, request):
        form = _CourseForm(request.POST)
        if form.is_valid():
            form.save()
            messages.success(request, 'Course added successfully.')
            return redirect('portal:course_list')
        return render(request, self.template_name, {'form': form, 'action': 'Add'})


class CourseUpdateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'courses'
    module_action = 'edit'
    template_name = 'admin_portal/courses/form.html'

    def get(self, request, pk):
        course = get_object_or_404(Course, pk=pk)
        return render(request, self.template_name, {
            'form': _CourseForm(instance=course), 'course': course, 'action': 'Edit',
        })

    def post(self, request, pk):
        course = get_object_or_404(Course, pk=pk)
        form = _CourseForm(request.POST, instance=course)
        if form.is_valid():
            form.save()
            messages.success(request, 'Course updated.')
            return redirect('portal:course_list')
        return render(request, self.template_name, {'form': form, 'course': course, 'action': 'Edit'})


class CourseDeleteView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'courses'
    module_action = 'delete'
    template_name = 'admin_portal/courses/confirm_delete.html'

    def get(self, request, pk):
        course = get_object_or_404(Course, pk=pk)
        return render(request, self.template_name, {'course': course})

    def post(self, request, pk):
        course = get_object_or_404(Course, pk=pk)
        name = course.name
        course.delete()
        messages.success(request, f'"{name}" deleted.')
        return redirect('portal:course_list')


class StudentCoursesView(StudentRequiredMixin, TemplateView):
    template_name = 'student/courses.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        enrollment = getattr(self.request.user, 'enrollment', None)
        q = self.request.GET.get('q', '').strip()
        if enrollment:
            qs = Course.objects.filter(program=enrollment.program, is_active=True)
            if q:
                qs = qs.filter(
                    Q(name__icontains=q) |
                    Q(code__icontains=q) |
                    Q(description__icontains=q)
                )
            courses_by_year = {}
            for course in qs.order_by('year', 'name'):
                courses_by_year.setdefault(course.year, []).append(course)
            ctx['courses_by_year'] = courses_by_year
            ctx['enrollment'] = enrollment
        ctx['search_query'] = q
        ctx['content_locked'] = bool(enrollment and not _student_has_access(enrollment))
        return ctx


# ---------------------------------------------------------------------------
# Payments (admin CRUD + student view)
# ---------------------------------------------------------------------------

class _PaymentForm(forms.ModelForm):
    class Meta:
        model = Payment
        fields = ['enrollment', 'amount', 'payment_type', 'term', 'academic_year',
                  'payment_method', 'reference', 'payment_date', 'notes']
        widgets = {
            'payment_date': forms.DateInput(attrs={'type': 'date'}),
            'notes': forms.Textarea(attrs={'rows': 2}),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['enrollment'].queryset = (
            Enrollment.objects.filter(is_active=True)
            .select_related('student', 'program')
            .order_by('student__first_name', 'student__last_name')
        )
        from crispy_forms.helper import FormHelper
        from crispy_forms.layout import Layout, Row, Column, Submit
        self.helper = FormHelper()
        self.helper.layout = Layout(
            'enrollment',
            Row(
                Column('amount', css_class='col-md-4'),
                Column('payment_type', css_class='col-md-4'),
                Column('payment_method', css_class='col-md-4'),
            ),
            Row(
                Column('term', css_class='col-md-4'),
                Column('academic_year', css_class='col-md-4'),
                Column('payment_date', css_class='col-md-4'),
            ),
            'reference',
            'notes',
            Submit('submit', 'Save Payment', css_class='btn btn-primary mt-2'),
        )


class PaymentListView(AdminRequiredMixin, ModulePermissionMixin, ListView):
    module = 'payments'
    model = Payment
    template_name = 'admin_portal/payments/list.html'
    context_object_name = 'payments'
    paginate_by = 25

    def get_queryset(self):
        qs = Payment.objects.select_related(
            'enrollment__student', 'enrollment__program', 'recorded_by'
        )
        if q := self.request.GET.get('q'):
            qs = qs.filter(
                Q(enrollment__student__first_name__icontains=q) |
                Q(enrollment__student__last_name__icontains=q) |
                Q(reference__icontains=q)
            )
        if term := self.request.GET.get('term'):
            qs = qs.filter(term=term)
        if year := self.request.GET.get('year'):
            qs = qs.filter(academic_year=year)
        if program := self.request.GET.get('program'):
            qs = qs.filter(enrollment__program_id=program)
        return qs

    def get(self, request, *args, **kwargs):
        if request.GET.get('export') == '1':
            qs = self.get_queryset()
            headers = ['Reference', 'Student Name', 'Student No.', 'Program',
                       'Amount (UGX)', 'Type', 'Term', 'Academic Year',
                       'Payment Date', 'Method', 'Recorded By']
            rows = [
                (
                    p.reference,
                    p.enrollment.student.get_full_name(),
                    p.enrollment.student_number,
                    p.enrollment.program.name,
                    float(p.amount),
                    p.get_payment_type_display(),
                    p.get_term_display(),
                    p.academic_year or '',
                    p.payment_date.strftime('%d/%m/%Y') if p.payment_date else '',
                    p.get_payment_method_display(),
                    p.recorded_by.get_full_name() if p.recorded_by else '',
                )
                for p in qs
            ]
            return excel_response('Payments', 'MLS_Payments', headers, rows)
        return super().get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['programs'] = Program.objects.filter(is_active=True)
        ctx['term_choices'] = Payment.TERM_CHOICES
        ctx['search_query'] = self.request.GET.get('q', '')
        ctx['current_term'] = self.request.GET.get('term', '')
        ctx['current_year'] = self.request.GET.get('year', '')
        ctx['current_program'] = self.request.GET.get('program', '')
        ctx['total_amount'] = self.object_list.aggregate(total=Sum('amount'))['total'] or 0
        return ctx


class PaymentCreateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'payments'
    module_action = 'edit'
    template_name = 'admin_portal/payments/form.html'

    def get(self, request):
        form = _PaymentForm(initial={
            'enrollment': request.GET.get('enrollment'),
            'academic_year': '2025/2026',
        })
        return render(request, self.template_name, {
            'form': form,
            'action': 'Record',
            'next_url': request.GET.get('next', ''),
        })

    def post(self, request):
        form = _PaymentForm(request.POST)
        if form.is_valid():
            payment = form.save(commit=False)
            payment.recorded_by = request.user
            payment.save()
            messages.success(request, 'Payment recorded successfully.')
            next_url = request.POST.get('next', '')
            return redirect(next_url if next_url else 'portal:payment_list')
        return render(request, self.template_name, {
            'form': form,
            'action': 'Record',
            'next_url': request.POST.get('next', ''),
        })


class PaymentUpdateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'payments'
    module_action = 'edit'
    template_name = 'admin_portal/payments/form.html'

    def get(self, request, pk):
        payment = get_object_or_404(Payment, pk=pk)
        return render(request, self.template_name, {
            'form': _PaymentForm(instance=payment),
            'payment': payment,
            'action': 'Edit',
            'next_url': request.GET.get('next', ''),
        })

    def post(self, request, pk):
        payment = get_object_or_404(Payment, pk=pk)
        form = _PaymentForm(request.POST, instance=payment)
        if form.is_valid():
            form.save()
            messages.success(request, 'Payment updated.')
            next_url = request.POST.get('next', '')
            return redirect(next_url if next_url else 'portal:payment_list')
        return render(request, self.template_name, {
            'form': form,
            'payment': payment,
            'action': 'Edit',
            'next_url': request.POST.get('next', ''),
        })


class PaymentDeleteView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'payments'
    module_action = 'delete'
    template_name = 'admin_portal/payments/confirm_delete.html'

    def get(self, request, pk):
        payment = get_object_or_404(Payment, pk=pk)
        return render(request, self.template_name, {'payment': payment})

    def post(self, request, pk):
        payment = get_object_or_404(Payment, pk=pk)
        student_name = payment.enrollment.student.get_full_name()
        payment.delete()
        messages.success(request, f'Payment for {student_name} deleted.')
        return redirect('portal:payment_list')


class StudentPaymentHistoryView(StudentRequiredMixin, TemplateView):
    template_name = 'student/payments.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        enrollment = getattr(self.request.user, 'enrollment', None)
        if enrollment:
            payments = Payment.objects.filter(enrollment=enrollment).order_by('-payment_date')
            total_paid = payments.aggregate(total=Sum('amount'))['total'] or 0
            ctx['payments'] = payments
            ctx['total_paid'] = total_paid
            ctx['enrollment'] = enrollment

            # Fee breakdown for the online payment panel
            program = enrollment.program
            tuition = float(program.tuition_fee or 0)
            practical = float(program.practical_fee or 0)
            assessment = float(program.assessment_fee or 0)
            exam = float(program.exam_fee or 0)
            total_fees = tuition + practical + assessment + exam

            paid_by_type = {
                pt: float(
                    Payment.objects.filter(enrollment=enrollment, payment_type=pt)
                    .aggregate(t=Sum('amount'))['t'] or 0
                )
                for pt, _ in Payment.PAYMENT_TYPE_CHOICES
            }
            breakdown = [
                {'key': Payment.TUITION,    'label': 'Tuition Fee',             'total': tuition,    'paid': paid_by_type[Payment.TUITION],    'outstanding': max(0, tuition    - paid_by_type[Payment.TUITION])},
                {'key': Payment.PRACTICAL,  'label': 'Practical / Project Fee', 'total': practical,  'paid': paid_by_type[Payment.PRACTICAL],  'outstanding': max(0, practical  - paid_by_type[Payment.PRACTICAL])},
                {'key': Payment.ASSESSMENT, 'label': 'Assessment Fee',          'total': assessment, 'paid': paid_by_type[Payment.ASSESSMENT], 'outstanding': max(0, assessment - paid_by_type[Payment.ASSESSMENT])},
            ]
            if exam > 0:
                breakdown.append({
                    'key': Payment.EXAM, 'label': 'Exam Fee',
                    'total': exam, 'paid': paid_by_type[Payment.EXAM],
                    'outstanding': max(0, exam - paid_by_type[Payment.EXAM]),
                    'is_exam': True,
                })
            ctx['fee_breakdown'] = breakdown
            ctx['has_exam_fee'] = exam > 0
            ctx['total_fees'] = total_fees
            ctx['total_outstanding'] = max(0, total_fees - float(total_paid))
            ctx['flw_public_key'] = settings.FLW_PUBLIC_KEY
            ctx['term_choices'] = Payment.TERM_CHOICES
            allowed_types = [Payment.TUITION, Payment.PRACTICAL, Payment.ASSESSMENT]
            if exam > 0:
                allowed_types.append(Payment.EXAM)
            ctx['payment_type_choices'] = [
                (k, v) for k, v in Payment.PAYMENT_TYPE_CHOICES if k in allowed_types
            ]
            ctx['default_academic_year'] = f'{_dt.date.today().year}/{_dt.date.today().year + 1}'
        return ctx


class StudentPaymentReceiptView(StudentRequiredMixin, TemplateView):
    template_name = 'student/payment_receipt.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        enrollment = getattr(self.request.user, 'enrollment', None)
        payment = get_object_or_404(Payment, pk=self.kwargs['pk'])
        if not enrollment or payment.enrollment_id != enrollment.pk:
            from django.core.exceptions import PermissionDenied
            raise PermissionDenied
        ctx['payment'] = payment
        ctx['enrollment'] = enrollment
        return ctx


class FlutterwaveInitiateView(StudentRequiredMixin, View):
    """AJAX endpoint: validates payment details, stores in session, returns JSON for JS checkout."""
    def post(self, request):
        from decimal import Decimal, InvalidOperation
        import uuid
        enrollment = getattr(request.user, 'enrollment', None)
        if not enrollment:
            return JsonResponse({'error': 'No enrollment found.'}, status=400)

        amount_str = request.POST.get('amount', '').strip()
        payment_type = request.POST.get('payment_type', '').strip()
        term = request.POST.get('term', '').strip()
        academic_year = request.POST.get('academic_year', '').strip()

        valid_types = dict(Payment.PAYMENT_TYPE_CHOICES)
        valid_terms = dict(Payment.TERM_CHOICES)

        try:
            amount = Decimal(amount_str)
            if amount < 1000:
                raise ValueError('Minimum 1,000 UGX')
        except (InvalidOperation, ValueError, AssertionError) as e:
            return JsonResponse({'error': f'Invalid amount: {e}'}, status=400)
        if payment_type not in valid_types:
            return JsonResponse({'error': 'Invalid payment type.'}, status=400)
        if term not in valid_terms:
            return JsonResponse({'error': 'Invalid term.'}, status=400)
        if not academic_year:
            return JsonResponse({'error': 'Academic year is required.'}, status=400)

        tx_ref = f'MLS-FEE-{uuid.uuid4().hex[:12].upper()}'
        request.session['pending_flw_payment'] = {
            'tx_ref': tx_ref,
            'enrollment_id': enrollment.pk,
            'amount': str(amount),
            'payment_type': payment_type,
            'term': term,
            'academic_year': academic_year,
        }

        return JsonResponse({
            'tx_ref': tx_ref,
            'public_key': settings.FLW_PUBLIC_KEY,
            'amount': float(amount),
            'customer': {
                'email': request.user.email or '',
                'phone_number': getattr(request.user, 'phone', '') or '',
                'name': request.user.get_full_name() or request.user.username,
            },
            'description': f'{valid_types[payment_type]} — {valid_terms[term]} {academic_year}',
            'callback_url': request.build_absolute_uri(reverse('portal:payment_callback')),
        })


class FlutterwaveStudentCallbackView(StudentRequiredMixin, View):
    """Handles Flutterwave redirect after student pays fees online."""
    def get(self, request):
        import requests as http_req
        from decimal import Decimal
        import uuid

        status = request.GET.get('status', '')
        tx_ref = request.GET.get('tx_ref', '')
        transaction_id = request.GET.get('transaction_id', '')

        if status != 'successful':
            messages.error(request, f'Payment was not completed (status: {status or "unknown"}). Please try again.')
            return redirect('portal:student_payments')

        if Payment.objects.filter(flw_tx_ref=tx_ref).exists():
            messages.info(request, 'This payment has already been recorded.')
            return redirect('portal:student_payments')

        try:
            resp = http_req.get(
                f'https://api.flutterwave.com/v3/transactions/{transaction_id}/verify',
                headers={'Authorization': f'Bearer {settings.FLW_SECRET_KEY}'},
                timeout=15,
            )
            flw = resp.json()
        except Exception:
            messages.error(request, f'Could not verify payment with Flutterwave. Please contact admin with reference: {tx_ref}')
            return redirect('portal:student_payments')

        if flw.get('status') != 'success':
            messages.error(request, f'Payment verification failed. Contact admin with reference: {tx_ref}')
            return redirect('portal:student_payments')

        pending = request.session.get('pending_flw_payment', {})
        enrollment = getattr(request.user, 'enrollment', None)

        if pending.get('tx_ref') == tx_ref and enrollment:
            pay_amount = Decimal(pending['amount'])
            pay_type = pending['payment_type']
            pay_term = pending['term']
            academic_year = pending['academic_year']
        else:
            flw_data = flw.get('data', {})
            pay_amount = Decimal(str(flw_data.get('amount', 0)))
            pay_type = Payment.TUITION
            pay_term = Payment.TERM_1
            academic_year = f'{_dt.date.today().year}/{_dt.date.today().year + 1}'

        if not enrollment:
            messages.error(request, f'Enrollment not found. Contact admin with reference: {tx_ref}')
            return redirect('portal:student_payments')

        Payment.objects.create(
            enrollment=enrollment,
            amount=pay_amount,
            payment_type=pay_type,
            term=pay_term,
            academic_year=academic_year,
            payment_method=Payment.ONLINE,
            flw_tx_ref=tx_ref,
            flw_transaction_id=str(transaction_id),
            reference=f'FLW-{transaction_id}',
            payment_date=_dt.date.today(),
            notes='Online payment via Flutterwave',
        )

        if 'pending_flw_payment' in request.session:
            del request.session['pending_flw_payment']

        messages.success(request, f'Payment of UGX {pay_amount:,.0f} received successfully! Your account has been updated.')
        return redirect('portal:student_payments')


# ---------------------------------------------------------------------------
# Timetable (admin CRUD + student view)
# ---------------------------------------------------------------------------

def _build_timetable_groups(entries):
    """Group a queryset/list of TimetableEntry into year → day nested structure."""
    all_entries = list(entries)
    result = []
    for y_num, y_name in TimetableEntry.YEAR_CHOICES:
        year_days = []
        for d_num, d_name in TimetableEntry.DAY_CHOICES:
            day_entries = [e for e in all_entries if e.year == y_num and e.day == d_num]
            if day_entries:
                year_days.append({'day_num': d_num, 'day_name': d_name, 'entries': day_entries})
        if year_days:
            result.append({'year_num': y_num, 'year_name': y_name, 'days': year_days})
    return result


class _CourseSelectWidget(forms.Select):
    """Select widget that tags each course option with its program id, so the
    Course dropdown can be filtered client-side by the chosen Program."""
    def create_option(self, name, value, label, selected, index, subindex=None, attrs=None):
        option = super().create_option(name, value, label, selected, index, subindex, attrs)
        instance = getattr(value, 'instance', None)
        if instance is not None and getattr(instance, 'program_id', None):
            option['attrs']['data-program'] = str(instance.program_id)
        return option


class _TimetableEntryForm(forms.ModelForm):
    class Meta:
        model = TimetableEntry
        fields = ['program', 'year', 'term', 'academic_year',
                  'course', 'subject', 'day', 'start_time', 'end_time', 'venue']
        widgets = {
            'start_time': forms.TimeInput(format='%H:%M', attrs={'type': 'time', 'class': 'form-control'}),
            'end_time': forms.TimeInput(format='%H:%M', attrs={'type': 'time', 'class': 'form-control'}),
            'course': _CourseSelectWidget(),
        }

    def __init__(self, *args, programs=None, **kwargs):
        super().__init__(*args, **kwargs)
        # When `programs` is supplied (teacher), restrict choices to those programs.
        program_qs = programs if programs is not None else Program.objects.filter(is_active=True)
        self.fields['program'].queryset = program_qs
        course_qs = (
            Course.objects.filter(is_active=True)
            .select_related('program')
            .order_by('program__name', 'year', 'name')
        )
        if programs is not None:
            course_qs = course_qs.filter(program__in=programs)
        self.fields['course'].queryset = course_qs
        from crispy_forms.helper import FormHelper
        from crispy_forms.layout import Layout, Row, Column, Submit, HTML
        self.helper = FormHelper()
        self.helper.layout = Layout(
            Row(
                Column('program', css_class='col-md-6'),
                Column('year', css_class='col-md-3'),
                Column('term', css_class='col-md-3'),
            ),
            'academic_year',
            HTML('<hr class="my-3"><p class="small text-muted mb-2">'
                 'Select a Course from the curriculum <em>or</em> type a Subject name manually.</p>'),
            Row(
                Column('course', css_class='col-md-6'),
                Column('subject', css_class='col-md-6'),
            ),
            HTML('<hr class="my-3">'),
            Row(
                Column('day', css_class='col-md-4'),
                Column('start_time', css_class='col-md-4'),
                Column('end_time', css_class='col-md-4'),
            ),
            'venue',
            Submit('submit', 'Save Entry', css_class='btn btn-primary mt-2'),
        )

    def clean(self):
        cleaned_data = super().clean()
        if not cleaned_data.get('course') and not cleaned_data.get('subject'):
            raise forms.ValidationError('Please select a Course or enter a Subject name.')
        return cleaned_data


class TimetableListView(AdminRequiredMixin, ModulePermissionMixin, ListView):
    module = 'timetable'
    model = TimetableEntry
    template_name = 'admin_portal/timetable/list.html'
    context_object_name = 'entries'
    paginate_by = None

    def get_queryset(self):
        qs = TimetableEntry.objects.select_related('program', 'course')
        if program := self.request.GET.get('program'):
            qs = qs.filter(program_id=program)
        if year := self.request.GET.get('year'):
            qs = qs.filter(year=year)
        if term := self.request.GET.get('term'):
            qs = qs.filter(term=term)
        if academic_year := self.request.GET.get('academic_year'):
            qs = qs.filter(academic_year=academic_year)
        return qs

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['programs'] = Program.objects.filter(is_active=True)
        ctx['term_choices'] = TimetableEntry.TERM_CHOICES
        ctx['year_choices'] = TimetableEntry.YEAR_CHOICES
        ctx['current_program'] = self.request.GET.get('program', '')
        ctx['current_year'] = self.request.GET.get('year', '')
        ctx['current_term'] = self.request.GET.get('term', '')
        ctx['current_academic_year'] = self.request.GET.get('academic_year', '')
        ctx['entries_by_year_day'] = _build_timetable_groups(self.object_list)
        return ctx


class TimetableCreateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'timetable'
    module_action = 'edit'
    template_name = 'admin_portal/timetable/form.html'

    def get(self, request):
        form = _TimetableEntryForm(initial={
            'program': request.GET.get('program'),
            'year': request.GET.get('year', 1),
            'term': request.GET.get('term', 'term_1'),
            'academic_year': '2025/2026',
        })
        return render(request, self.template_name, {'form': form, 'action': 'Add'})

    def post(self, request):
        form = _TimetableEntryForm(request.POST)
        if form.is_valid():
            form.save()
            messages.success(request, 'Timetable entry added.')
            return redirect('portal:timetable_list')
        return render(request, self.template_name, {'form': form, 'action': 'Add'})


class TimetableUpdateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'timetable'
    module_action = 'edit'
    template_name = 'admin_portal/timetable/form.html'

    def get(self, request, pk):
        entry = get_object_or_404(TimetableEntry, pk=pk)
        return render(request, self.template_name, {
            'form': _TimetableEntryForm(instance=entry), 'entry': entry, 'action': 'Edit',
        })

    def post(self, request, pk):
        entry = get_object_or_404(TimetableEntry, pk=pk)
        form = _TimetableEntryForm(request.POST, instance=entry)
        if form.is_valid():
            form.save()
            messages.success(request, 'Entry updated.')
            return redirect('portal:timetable_list')
        return render(request, self.template_name, {'form': form, 'entry': entry, 'action': 'Edit'})


class TimetableDeleteView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'timetable'
    module_action = 'delete'
    template_name = 'admin_portal/timetable/confirm_delete.html'

    def get(self, request, pk):
        entry = get_object_or_404(TimetableEntry, pk=pk)
        return render(request, self.template_name, {'entry': entry})

    def post(self, request, pk):
        entry = get_object_or_404(TimetableEntry, pk=pk)
        entry.delete()
        messages.success(request, 'Timetable entry deleted.')
        return redirect('portal:timetable_list')


class StudentTimetableView(StudentRequiredMixin, TemplateView):
    template_name = 'student/timetable.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        enrollment = getattr(self.request.user, 'enrollment', None)
        if enrollment:
            term = self.request.GET.get('term', '')
            academic_year = self.request.GET.get('academic_year', '')
            year_group = self.request.GET.get('year', '')

            qs = TimetableEntry.objects.filter(
                program=enrollment.program
            ).select_related('course')

            if term:
                qs = qs.filter(term=term)
            if academic_year:
                qs = qs.filter(academic_year=academic_year)
            if year_group:
                qs = qs.filter(year=year_group)

            ctx['entries_by_year_day'] = _build_timetable_groups(qs)
            ctx['enrollment'] = enrollment
            ctx['term_choices'] = TimetableEntry.TERM_CHOICES
            ctx['year_choices'] = TimetableEntry.YEAR_CHOICES
            ctx['current_term'] = term
            ctx['current_academic_year'] = academic_year
            ctx['current_year'] = year_group
        return ctx


# ---------------------------------------------------------------------------
# Certificates (admin CRUD + student view)
# ---------------------------------------------------------------------------

class _CertificateForm(forms.ModelForm):
    class Meta:
        model = Certificate
        fields = ['enrollment', 'certificate_number', 'issue_date', 'academic_year', 'certificate_file', 'notes']
        widgets = {
            'issue_date': forms.DateInput(attrs={'type': 'date'}),
            'notes': forms.Textarea(attrs={'rows': 2}),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.fields['enrollment'].queryset = (
            Enrollment.objects.filter(is_active=True)
            .select_related('student', 'program')
            .order_by('student__first_name', 'student__last_name')
        )
        from crispy_forms.helper import FormHelper
        from crispy_forms.layout import Layout, Row, Column, Submit
        self.helper = FormHelper()
        self.helper.layout = Layout(
            'enrollment',
            Row(
                Column('certificate_number', css_class='col-md-6'),
                Column('issue_date', css_class='col-md-3'),
                Column('academic_year', css_class='col-md-3'),
            ),
            'certificate_file',
            'notes',
            Submit('submit', 'Save Certificate', css_class='btn btn-primary mt-2'),
        )


class CertificateListView(AdminRequiredMixin, ModulePermissionMixin, ListView):
    module = 'certificates'
    model = Certificate
    template_name = 'admin_portal/certificates/list.html'
    context_object_name = 'certificates'
    paginate_by = 25

    def get_queryset(self):
        qs = Certificate.objects.select_related(
            'enrollment__student', 'enrollment__program', 'recorded_by'
        )
        if q := self.request.GET.get('q'):
            qs = qs.filter(
                Q(enrollment__student__first_name__icontains=q) |
                Q(enrollment__student__last_name__icontains=q) |
                Q(certificate_number__icontains=q)
            )
        if program := self.request.GET.get('program'):
            qs = qs.filter(enrollment__program_id=program)
        if year := self.request.GET.get('year'):
            qs = qs.filter(academic_year=year)
        return qs

    def get(self, request, *args, **kwargs):
        if request.GET.get('export') == '1':
            qs = self.get_queryset()
            headers = ['Cert No.', 'Student Name', 'Student No.',
                       'Program', 'Academic Year', 'Issue Date', 'Notes']
            rows = [
                (
                    c.certificate_number,
                    c.enrollment.student.get_full_name(),
                    c.enrollment.student_number,
                    c.enrollment.program.name,
                    c.academic_year or '',
                    c.issue_date.strftime('%d/%m/%Y') if c.issue_date else '',
                    c.notes or '',
                )
                for c in qs
            ]
            return excel_response('Certificates', 'MLS_Certificates', headers, rows)
        return super().get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['programs'] = Program.objects.filter(is_active=True)
        ctx['search_query'] = self.request.GET.get('q', '')
        ctx['current_program'] = self.request.GET.get('program', '')
        ctx['current_year'] = self.request.GET.get('year', '')
        return ctx


class CertificateCreateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'certificates'
    module_action = 'edit'
    template_name = 'admin_portal/certificates/form.html'

    def get(self, request):
        form = _CertificateForm(initial={
            'enrollment': request.GET.get('enrollment'),
            'academic_year': '2025/2026',
        })
        return render(request, self.template_name, {
            'form': form,
            'action': 'Record',
            'next_url': request.GET.get('next', ''),
        })

    def post(self, request):
        form = _CertificateForm(request.POST, request.FILES)
        if form.is_valid():
            cert = form.save(commit=False)
            cert.recorded_by = request.user
            cert.save()
            messages.success(request, 'Certificate recorded successfully.')
            next_url = request.POST.get('next', '')
            return redirect(next_url if next_url else 'portal:certificate_list')
        return render(request, self.template_name, {
            'form': form,
            'action': 'Record',
            'next_url': request.POST.get('next', ''),
        })


class CertificateUpdateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'certificates'
    module_action = 'edit'
    template_name = 'admin_portal/certificates/form.html'

    def get(self, request, pk):
        cert = get_object_or_404(Certificate, pk=pk)
        return render(request, self.template_name, {
            'form': _CertificateForm(instance=cert),
            'cert': cert,
            'action': 'Edit',
            'next_url': request.GET.get('next', ''),
        })

    def post(self, request, pk):
        cert = get_object_or_404(Certificate, pk=pk)
        form = _CertificateForm(request.POST, request.FILES, instance=cert)
        if form.is_valid():
            form.save()
            messages.success(request, 'Certificate updated.')
            next_url = request.POST.get('next', '')
            return redirect(next_url if next_url else 'portal:certificate_list')
        return render(request, self.template_name, {
            'form': form,
            'cert': cert,
            'action': 'Edit',
            'next_url': request.POST.get('next', ''),
        })


class CertificateDeleteView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'certificates'
    module_action = 'delete'
    template_name = 'admin_portal/certificates/confirm_delete.html'

    def get(self, request, pk):
        cert = get_object_or_404(Certificate, pk=pk)
        return render(request, self.template_name, {'cert': cert})

    def post(self, request, pk):
        cert = get_object_or_404(Certificate, pk=pk)
        student_name = cert.enrollment.student.get_full_name()
        cert.delete()
        messages.success(request, f'Certificate for {student_name} deleted.')
        return redirect('portal:certificate_list')


class StudentCertificatesView(StudentRequiredMixin, TemplateView):
    template_name = 'student/certificates.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        enrollment = getattr(self.request.user, 'enrollment', None)
        if enrollment:
            ctx['certificates'] = Certificate.objects.filter(
                enrollment=enrollment
            ).order_by('-issue_date')
            ctx['enrollment'] = enrollment
        return ctx


class StudentCertificatePrintView(StudentRequiredMixin, TemplateView):
    template_name = 'student/certificate_print.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        enrollment = getattr(self.request.user, 'enrollment', None)
        cert = get_object_or_404(Certificate, pk=self.kwargs['pk'])
        if not enrollment or cert.enrollment_id != enrollment.pk:
            raise PermissionDenied
        import datetime
        ctx['cert'] = cert
        ctx['enrollment'] = enrollment
        ctx['student'] = self.request.user
        ctx['printed_date'] = datetime.date.today()
        return ctx


# ---------------------------------------------------------------------------
# Teacher portal views
# ---------------------------------------------------------------------------

def _teacher_programs(user):
    profile = getattr(user, 'teacher_profile', None)
    return profile.programs.filter(is_active=True) if profile else Program.objects.none()


class TeacherCoursesView(TeacherRequiredMixin, TemplateView):
    template_name = 'teacher/courses.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        programs = _teacher_programs(self.request.user)
        q = self.request.GET.get('q', '').strip()
        qs = Course.objects.filter(program__in=programs, is_active=True).select_related('program')
        if q:
            qs = qs.filter(
                Q(name__icontains=q) |
                Q(code__icontains=q) |
                Q(description__icontains=q)
            )
        all_courses = list(qs.order_by('program__name', 'year', 'name'))
        courses_by_program = {}
        for prog in programs:
            prog_courses = [c for c in all_courses if c.program_id == prog.pk]
            if prog_courses:
                courses_by_program[prog] = prog_courses
        ctx['courses_by_program'] = courses_by_program
        ctx['total_courses'] = len(all_courses)
        ctx['search_query'] = q
        return ctx


class TeacherStudentsView(TeacherRequiredMixin, ListView):
    template_name = 'teacher/students.html'
    context_object_name = 'enrollments'
    paginate_by = 20

    def get_queryset(self):
        programs = _teacher_programs(self.request.user)
        qs = Enrollment.objects.filter(
            program__in=programs, is_active=True
        ).select_related('student', 'program').order_by('program__name', 'student__first_name')
        if q := self.request.GET.get('q'):
            qs = qs.filter(
                Q(student__first_name__icontains=q) |
                Q(student__last_name__icontains=q) |
                Q(student_number__icontains=q)
            )
        if program := self.request.GET.get('program'):
            qs = qs.filter(program_id=program)
        return qs

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['my_programs'] = _teacher_programs(self.request.user)
        ctx['search_query'] = self.request.GET.get('q', '')
        ctx['current_program'] = self.request.GET.get('program', '')
        return ctx


class TeacherTimetableView(TeacherRequiredMixin, TemplateView):
    template_name = 'teacher/timetable.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        programs = _teacher_programs(self.request.user)
        term = self.request.GET.get('term', '')
        academic_year = self.request.GET.get('academic_year', '')
        year_group = self.request.GET.get('year', '')
        program_filter = self.request.GET.get('program', '')

        qs = TimetableEntry.objects.filter(
            program__in=programs
        ).select_related('program', 'course')
        if term:
            qs = qs.filter(term=term)
        if academic_year:
            qs = qs.filter(academic_year=academic_year)
        if year_group:
            qs = qs.filter(year=year_group)
        if program_filter:
            qs = qs.filter(program_id=program_filter)

        all_entries = list(qs)
        program_timetables = []
        for prog in programs:
            prog_entries = [e for e in all_entries if e.program_id == prog.pk]
            if prog_entries:
                program_timetables.append({
                    'program': prog,
                    'groups': _build_timetable_groups(prog_entries),
                })

        ctx['program_timetables'] = program_timetables
        ctx['programs'] = programs
        ctx['term_choices'] = TimetableEntry.TERM_CHOICES
        ctx['year_choices'] = TimetableEntry.YEAR_CHOICES
        ctx['current_term'] = term
        ctx['current_academic_year'] = academic_year
        ctx['current_year'] = year_group
        ctx['current_program'] = program_filter
        ctx['can_manage'] = programs.exists()
        return ctx


# Teacher timetable management — scoped to the teacher's assigned programs.

class TeacherTimetableCreateView(TeacherRequiredMixin, View):
    template_name = 'teacher/timetable_form.html'

    def get(self, request):
        programs = _teacher_programs(request.user)
        form = _TimetableEntryForm(programs=programs, initial={
            'program': request.GET.get('program'),
            'year': request.GET.get('year', 1),
            'term': request.GET.get('term', 'term_1'),
            'academic_year': '2025/2026',
        })
        return render(request, self.template_name, {'form': form, 'action': 'Add'})

    def post(self, request):
        programs = _teacher_programs(request.user)
        form = _TimetableEntryForm(request.POST, programs=programs)
        if form.is_valid():
            entry = form.save(commit=False)
            if not programs.filter(pk=entry.program_id).exists():
                messages.error(request, 'You can only add entries for your assigned programs.')
                return render(request, self.template_name, {'form': form, 'action': 'Add'})
            entry.save()
            messages.success(request, 'Timetable entry added.')
            return redirect('portal:teacher_timetable')
        return render(request, self.template_name, {'form': form, 'action': 'Add'})


class TeacherTimetableUpdateView(TeacherRequiredMixin, View):
    template_name = 'teacher/timetable_form.html'

    def get(self, request, pk):
        programs = _teacher_programs(request.user)
        entry = get_object_or_404(TimetableEntry, pk=pk)
        if not programs.filter(pk=entry.program_id).exists():
            messages.error(request, 'You can only edit entries for your assigned programs.')
            return redirect('portal:teacher_timetable')
        return render(request, self.template_name, {
            'form': _TimetableEntryForm(instance=entry, programs=programs),
            'entry': entry, 'action': 'Edit',
        })

    def post(self, request, pk):
        programs = _teacher_programs(request.user)
        entry = get_object_or_404(TimetableEntry, pk=pk)
        if not programs.filter(pk=entry.program_id).exists():
            messages.error(request, 'You can only edit entries for your assigned programs.')
            return redirect('portal:teacher_timetable')
        form = _TimetableEntryForm(request.POST, instance=entry, programs=programs)
        if form.is_valid():
            updated = form.save(commit=False)
            if not programs.filter(pk=updated.program_id).exists():
                messages.error(request, 'You can only assign entries to your assigned programs.')
                return render(request, self.template_name, {'form': form, 'entry': entry, 'action': 'Edit'})
            updated.save()
            messages.success(request, 'Timetable entry updated.')
            return redirect('portal:teacher_timetable')
        return render(request, self.template_name, {'form': form, 'entry': entry, 'action': 'Edit'})


class TeacherTimetableDeleteView(TeacherRequiredMixin, View):
    template_name = 'teacher/timetable_confirm_delete.html'

    def get(self, request, pk):
        programs = _teacher_programs(request.user)
        entry = get_object_or_404(TimetableEntry, pk=pk)
        if not programs.filter(pk=entry.program_id).exists():
            messages.error(request, 'You can only delete entries for your assigned programs.')
            return redirect('portal:teacher_timetable')
        return render(request, self.template_name, {'entry': entry})

    def post(self, request, pk):
        programs = _teacher_programs(request.user)
        entry = get_object_or_404(TimetableEntry, pk=pk)
        if not programs.filter(pk=entry.program_id).exists():
            messages.error(request, 'You can only delete entries for your assigned programs.')
            return redirect('portal:teacher_timetable')
        entry.delete()
        messages.success(request, 'Timetable entry deleted.')
        return redirect('portal:teacher_timetable')


# ---------------------------------------------------------------------------
# Programs (admin CRUD)
# ---------------------------------------------------------------------------

class _ProgramForm(forms.ModelForm):
    class Meta:
        model = Program
        fields = ['name', 'level', 'duration', 'description',
                  'tuition_fee', 'practical_fee', 'assessment_fee', 'exam_fee',
                  'entry_requirement', 'content_access_percentage', 'is_active']
        widgets = {'description': forms.Textarea(attrs={'rows': 3})}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        from crispy_forms.helper import FormHelper
        from crispy_forms.layout import Layout, Row, Column, Submit, Field, HTML
        self.helper = FormHelper()
        self.helper.layout = Layout(
            Row(
                Column('name', css_class='col-md-8'),
                Column('level', css_class='col-md-4'),
            ),
            Row(
                Column('duration', css_class='col-md-4'),
                Column('entry_requirement', css_class='col-md-8'),
            ),
            'description',
            Row(
                Column('tuition_fee', css_class='col-md-4'),
                Column('practical_fee', css_class='col-md-4'),
                Column('assessment_fee', css_class='col-md-4'),
            ),
            Row(
                Column('exam_fee', css_class='col-md-4'),
                Column(
                    HTML('<div class="alert alert-info py-2 px-3 small mt-4 mb-0">'
                         '<i class="fa-solid fa-circle-info me-1"></i>'
                         '<strong>Exam fees</strong> are shown separately to students. '
                         'Set to 0 if this program has no exam fee.</div>'),
                    css_class='col-md-8',
                ),
            ),
            Row(
                Column('content_access_percentage', css_class='col-md-4'),
                Column(Field('is_active'), css_class='col-md-8 pt-4'),
            ),
            Submit('submit', 'Save Program', css_class='btn btn-primary mt-2'),
        )


class ProgramListView(AdminRequiredMixin, ModulePermissionMixin, ListView):
    module = 'programs'
    model = Program
    template_name = 'admin_portal/programs/list.html'
    context_object_name = 'programs'
    paginate_by = None

    def get_queryset(self):
        return Program.objects.annotate(
            student_count=Count('enrollments', filter=Q(enrollments__is_active=True)),
            course_count=Count('courses', distinct=True),
        ).order_by('level', 'name')


class ProgramCreateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'programs'
    module_action = 'edit'
    template_name = 'admin_portal/programs/form.html'

    def get(self, request):
        return render(request, self.template_name, {'form': _ProgramForm(), 'action': 'Add'})

    def post(self, request):
        form = _ProgramForm(request.POST)
        if form.is_valid():
            form.save()
            messages.success(request, 'Program created successfully.')
            return redirect('portal:program_list')
        return render(request, self.template_name, {'form': form, 'action': 'Add'})


class ProgramUpdateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'programs'
    module_action = 'edit'
    template_name = 'admin_portal/programs/form.html'

    def get(self, request, pk):
        program = get_object_or_404(Program, pk=pk)
        return render(request, self.template_name, {
            'form': _ProgramForm(instance=program), 'program': program, 'action': 'Edit',
        })

    def post(self, request, pk):
        program = get_object_or_404(Program, pk=pk)
        form = _ProgramForm(request.POST, instance=program)
        if form.is_valid():
            form.save()
            messages.success(request, 'Program updated.')
            return redirect('portal:program_list')
        return render(request, self.template_name, {
            'form': form, 'program': program, 'action': 'Edit',
        })


class ProgramToggleView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'programs'
    module_action = 'edit'
    def post(self, request, pk):
        program = get_object_or_404(Program, pk=pk)
        program.is_active = not program.is_active
        program.save(update_fields=['is_active'])
        status = 'activated' if program.is_active else 'deactivated'
        messages.success(request, f'"{program.name}" {status}.')
        return redirect('portal:program_list')


# ---------------------------------------------------------------------------
# Staff / Teacher management (admin)
# ---------------------------------------------------------------------------

class _TeacherAssignForm(forms.Form):
    programs = forms.ModelMultipleChoiceField(
        queryset=Program.objects.filter(is_active=True),
        widget=forms.CheckboxSelectMultiple,
        required=False,
        label='Assigned Programs',
        help_text='Tick the programs this teacher is responsible for.',
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        from crispy_forms.helper import FormHelper
        from crispy_forms.layout import Layout, Submit
        self.helper = FormHelper()
        self.helper.layout = Layout(
            'programs',
            Submit('submit', 'Save Assignments', css_class='btn btn-primary mt-3'),
        )


class StaffListView(AdminRequiredMixin, ModulePermissionMixin, ListView):
    module = 'staff'
    template_name = 'admin_portal/staff/list.html'
    context_object_name = 'teachers'
    paginate_by = 20

    def get_queryset(self):
        return (
            User.objects.filter(role=User.TEACHER)
            .select_related('teacher_profile')
            .prefetch_related('teacher_profile__programs')
            .order_by('first_name', 'last_name')
        )

    def get(self, request, *args, **kwargs):
        if request.GET.get('export') == '1':
            qs = self.get_queryset()
            headers = ['Full Name', 'Email', 'Phone', 'Joined Date', 'Programs Assigned']
            rows = [
                (
                    t.get_full_name(),
                    t.email,
                    t.phone or '',
                    t.date_joined.strftime('%d/%m/%Y') if t.date_joined else '',
                    ', '.join(
                        p.name for p in t.teacher_profile.programs.all()
                    ) if hasattr(t, 'teacher_profile') and t.teacher_profile else '',
                )
                for t in qs
            ]
            return excel_response('Staff', 'MLS_Staff', headers, rows)
        return super().get(request, *args, **kwargs)


class StaffDetailView(AdminRequiredMixin, ModulePermissionMixin, TemplateView):
    module = 'staff'
    template_name = 'admin_portal/staff/detail.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        teacher = get_object_or_404(User, pk=self.kwargs['pk'], role=User.TEACHER)
        profile = getattr(teacher, 'teacher_profile', None)
        ctx['teacher'] = teacher
        ctx['profile'] = profile
        ctx['assigned_programs'] = profile.programs.all() if profile else Program.objects.none()
        return ctx


class StaffEditView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'staff'
    module_action = 'edit'
    template_name = 'admin_portal/staff/edit.html'

    def _get_objects(self, pk):
        teacher = get_object_or_404(User, pk=pk, role=User.TEACHER)
        profile, _ = TeacherProfile.objects.get_or_create(user=teacher)
        return teacher, profile

    def get(self, request, pk):
        teacher, profile = self._get_objects(pk)
        form = _TeacherAssignForm(initial={'programs': profile.programs.all()})
        return render(request, self.template_name, {'form': form, 'teacher': teacher})

    def post(self, request, pk):
        teacher, profile = self._get_objects(pk)
        form = _TeacherAssignForm(request.POST)
        if form.is_valid():
            profile.programs.set(form.cleaned_data['programs'])
            messages.success(request, f'Program assignments updated for {teacher.get_full_name()}.')
            return redirect('portal:staff_list')
        return render(request, self.template_name, {'form': form, 'teacher': teacher})


# ---------------------------------------------------------------------------
# Notifications (admin compose + list)
# ---------------------------------------------------------------------------

class _EnrollmentChoiceField(forms.ModelChoiceField):
    def label_from_instance(self, obj):
        return f'{obj.student.get_full_name()} — {obj.program.name} ({obj.student_number})'


class _NotificationComposeForm(forms.Form):
    AUDIENCE_ALL = 'all'
    AUDIENCE_PROGRAM = 'program'
    AUDIENCE_STUDENT = 'student'
    AUDIENCE_CHOICES = [
        (AUDIENCE_ALL, 'All active students'),
        (AUDIENCE_PROGRAM, 'Students in a specific program'),
        (AUDIENCE_STUDENT, 'A specific student'),
    ]
    title = forms.CharField(max_length=200)
    message = forms.CharField(widget=forms.Textarea(attrs={'rows': 4}))
    audience = forms.ChoiceField(choices=AUDIENCE_CHOICES)
    program = forms.ModelChoiceField(
        queryset=Program.objects.filter(is_active=True),
        required=False,
        empty_label='— Select program —',
    )
    student_enrollment = _EnrollmentChoiceField(
        queryset=Enrollment.objects.filter(is_active=True)
            .select_related('student', 'program')
            .order_by('student__first_name', 'student__last_name'),
        required=False,
        empty_label='— Select student —',
        label='Student',
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        from crispy_forms.helper import FormHelper
        from crispy_forms.layout import Layout, Row, Column, Submit
        self.helper = FormHelper()
        self.helper.layout = Layout(
            'title',
            'message',
            'audience',
            Row(
                Column('program', css_class='col-md-6'),
                Column('student_enrollment', css_class='col-md-6'),
            ),
            Submit('submit', 'Send Notification', css_class='btn btn-primary mt-2'),
        )


class NotificationListView(AdminRequiredMixin, ModulePermissionMixin, ListView):
    module = 'notifications'
    template_name = 'admin_portal/notifications/list.html'
    context_object_name = 'notifications'
    paginate_by = 30

    def get_queryset(self):
        return Notification.objects.select_related('recipient').order_by('-created_at')


class NotificationToggleReadView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'notifications'
    module_action = 'edit'
    """Flip a notification's read/unread state from the admin log."""
    def post(self, request, pk):
        notif = get_object_or_404(Notification, pk=pk)
        notif.is_read = not notif.is_read
        notif.save(update_fields=['is_read'])
        messages.success(
            request,
            f'Notification marked as {"read" if notif.is_read else "unread"}.',
        )
        return redirect('portal:notification_list')


class NotificationComposeView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'notifications'
    module_action = 'edit'
    template_name = 'admin_portal/notifications/compose.html'

    def get(self, request):
        return render(request, self.template_name, {'form': _NotificationComposeForm()})

    def post(self, request):
        form = _NotificationComposeForm(request.POST)
        if not form.is_valid():
            return render(request, self.template_name, {'form': form})

        cd = form.cleaned_data
        title, body, audience = cd['title'], cd['message'], cd['audience']

        if audience == _NotificationComposeForm.AUDIENCE_ALL:
            recipients = User.objects.filter(role=User.STUDENT, is_active=True)
        elif audience == _NotificationComposeForm.AUDIENCE_PROGRAM and cd.get('program'):
            recipients = User.objects.filter(
                enrollment__program=cd['program'], enrollment__is_active=True,
            )
        elif audience == _NotificationComposeForm.AUDIENCE_STUDENT and cd.get('student_enrollment'):
            recipients = User.objects.filter(pk=cd['student_enrollment'].student_id)
        else:
            messages.error(request, 'Please select a valid audience.')
            return render(request, self.template_name, {'form': form})

        count = 0
        for user in recipients:
            Notification.objects.create(recipient=user, title=title, message=body)
            count += 1

        messages.success(request, f'Notification sent to {count} recipient{"s" if count != 1 else ""}.')
        return redirect('portal:notification_list')


# ---------------------------------------------------------------------------
# Direct Enrollment (admin: create student account + enrollment in one step)
# ---------------------------------------------------------------------------

class _DirectEnrollForm(forms.Form):
    first_name = forms.CharField(max_length=150)
    last_name = forms.CharField(max_length=150)
    email = forms.EmailField()
    phone = forms.CharField(max_length=20, required=False)
    program = forms.ModelChoiceField(
        queryset=Program.objects.filter(is_active=True),
        empty_label='— Select program —',
    )

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        from crispy_forms.helper import FormHelper
        from crispy_forms.layout import Layout, Row, Column, Submit
        self.helper = FormHelper()
        self.helper.layout = Layout(
            Row(
                Column('first_name', css_class='col-md-6'),
                Column('last_name', css_class='col-md-6'),
            ),
            Row(
                Column('email', css_class='col-md-8'),
                Column('phone', css_class='col-md-4'),
            ),
            'program',
            Submit('submit', 'Enroll Student', css_class='btn btn-primary mt-2'),
        )

    def clean_email(self):
        email = self.cleaned_data['email'].lower().strip()
        if User.objects.filter(email__iexact=email).exists():
            raise forms.ValidationError('A user with this email already exists.')
        return email


class DirectEnrollView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'direct_enroll'
    module_action = 'edit'
    template_name = 'admin_portal/direct_enroll.html'

    @transaction.atomic
    def post(self, request):
        form = _DirectEnrollForm(request.POST)
        if not form.is_valid():
            return render(request, self.template_name, {'form': form})

        cd = form.cleaned_data
        temp_password = User.objects.make_random_password(length=10)
        user = User.objects.create_user(
            username=cd['email'],
            email=cd['email'],
            first_name=cd['first_name'],
            last_name=cd['last_name'],
            phone=cd.get('phone', ''),
            role=User.STUDENT,
            password=temp_password,
            is_verified=True,
        )
        enrollment = Enrollment.objects.create(
            student=user,
            application=None,
            program=cd['program'],
        )
        StudentProfile.objects.create(user=user)
        create_notification(
            request.user,
            'Student Directly Enrolled',
            f'{user.get_full_name()} enrolled into {cd["program"]}. Student number: {enrollment.student_number}.',
        )
        messages.success(
            request,
            f'Student enrolled successfully. Student number: {enrollment.student_number}. '
            f'Temporary password: {temp_password} — please share this securely with the student.',
        )
        return redirect('portal:student_detail', pk=enrollment.pk)

    def get(self, request):
        form = _DirectEnrollForm()
        return render(request, self.template_name, {'form': form})


# ---------------------------------------------------------------------------
# Donations (admin read-only viewer)
# ---------------------------------------------------------------------------

class DonationListView(AdminRequiredMixin, ModulePermissionMixin, ListView):
    module = 'donations'
    model = Donation
    template_name = 'admin_portal/donations/list.html'
    context_object_name = 'donations'
    paginate_by = 25

    def get_queryset(self):
        qs = Donation.objects.all()
        if q := self.request.GET.get('q', '').strip():
            qs = qs.filter(
                Q(name__icontains=q) |
                Q(email__icontains=q) |
                Q(flw_tx_ref__icontains=q)
            )
        if status := self.request.GET.get('status', '').strip():
            qs = qs.filter(status=status)
        return qs

    def get(self, request, *args, **kwargs):
        if request.GET.get('export') == '1':
            qs = self.get_queryset()
            headers = ['Tx Ref', 'Donor Name', 'Email', 'Phone',
                       'Amount (UGX)', 'Status', 'Date']
            rows = [
                (
                    d.flw_tx_ref,
                    d.name,
                    d.email,
                    getattr(d, 'phone', '') or '',
                    float(d.amount),
                    d.get_status_display(),
                    d.created_at.strftime('%d/%m/%Y') if d.created_at else '',
                )
                for d in qs
            ]
            return excel_response('Donations', 'MLS_Donations', headers, rows)
        return super().get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['status_choices'] = Donation.STATUS_CHOICES
        ctx['search_query'] = self.request.GET.get('q', '')
        ctx['current_status'] = self.request.GET.get('status', '')
        completed_qs = Donation.objects.filter(status=Donation.COMPLETED)
        ctx['total_completed'] = completed_qs.aggregate(total=Sum('amount'))['total'] or 0
        ctx['completed_count'] = completed_qs.count()
        ctx['pending_count'] = Donation.objects.filter(status=Donation.PENDING).count()
        return ctx


class DonationDeleteView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'donations'
    module_action = 'delete'
    """Delete a pending or failed donation. Completed donations are protected."""

    def post(self, request, pk):
        donation = get_object_or_404(Donation, pk=pk)
        if donation.status == Donation.COMPLETED:
            messages.error(request, 'Completed donations cannot be deleted.')
            return redirect('portal:donation_list')
        ref = donation.flw_tx_ref
        donation.delete()
        messages.success(request, f'Donation {ref} has been removed.')
        return redirect('portal:donation_list')


# ---------------------------------------------------------------------------
# Gallery (admin CRUD)
# ---------------------------------------------------------------------------

class _GalleryImageForm(forms.ModelForm):
    class Meta:
        model = GalleryImage
        # display_order is assigned automatically (by entry order), not by the admin.
        fields = ['title', 'description', 'image', 'category', 'is_published']
        widgets = {
            'description': forms.Textarea(attrs={'rows': 2}),
        }

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        from crispy_forms.helper import FormHelper
        from crispy_forms.layout import Column, Field, Layout, Row, Submit
        self.helper = FormHelper()
        self.helper.layout = Layout(
            'title',
            'image',
            'description',
            Row(
                Column('category', css_class='col-md-8'),
                Column(Field('is_published'), css_class='col-md-4 pt-4'),
            ),
            Submit('submit', 'Save', css_class='btn btn-primary mt-2'),
        )


class GalleryListView(AdminRequiredMixin, ModulePermissionMixin, ListView):
    module = 'gallery'
    model = GalleryImage
    template_name = 'admin_portal/gallery/list.html'
    context_object_name = 'gallery_images'
    paginate_by = 24
    ordering = ['display_order', '-created_at']


class GalleryCreateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'gallery'
    module_action = 'edit'
    template_name = 'admin_portal/gallery/form.html'

    def get(self, request):
        return render(request, self.template_name, {'form': _GalleryImageForm(), 'action': 'Add'})

    def post(self, request):
        form = _GalleryImageForm(request.POST, request.FILES)
        if form.is_valid():
            img = form.save(commit=False)
            # Auto-assign display order = next in sequence (entry order).
            last = GalleryImage.objects.order_by('-display_order').first()
            img.display_order = (last.display_order + 1) if last else 1
            img.save()
            messages.success(request, 'Image added to gallery.')
            return redirect('portal:gallery_list')
        return render(request, self.template_name, {'form': form, 'action': 'Add'})


class GalleryUpdateView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'gallery'
    module_action = 'edit'
    template_name = 'admin_portal/gallery/form.html'

    def get(self, request, pk):
        img = get_object_or_404(GalleryImage, pk=pk)
        return render(request, self.template_name, {'form': _GalleryImageForm(instance=img), 'img': img, 'action': 'Edit'})

    def post(self, request, pk):
        img = get_object_or_404(GalleryImage, pk=pk)
        form = _GalleryImageForm(request.POST, request.FILES, instance=img)
        if form.is_valid():
            form.save()
            messages.success(request, 'Gallery image updated.')
            return redirect('portal:gallery_list')
        return render(request, self.template_name, {'form': form, 'img': img, 'action': 'Edit'})


class GalleryDeleteView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'gallery'
    module_action = 'delete'
    def post(self, request, pk):
        img = get_object_or_404(GalleryImage, pk=pk)
        title = img.title
        if img.image:
            img.image.delete(save=False)
        img.delete()
        messages.success(request, f'"{title}" removed from gallery.')
        return redirect('portal:gallery_list')


class GalleryToggleView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'gallery'
    module_action = 'edit'
    def post(self, request, pk):
        img = get_object_or_404(GalleryImage, pk=pk)
        img.is_published = not img.is_published
        img.save(update_fields=['is_published'])
        status = 'published' if img.is_published else 'unpublished'
        messages.success(request, f'"{img.title}" {status}.')
        return redirect('portal:gallery_list')


# ---------------------------------------------------------------------------
# Reports (admin read-only)
# ---------------------------------------------------------------------------

class ReportsView(AdminRequiredMixin, ModulePermissionMixin, TemplateView):
    module = 'reports'
    template_name = 'admin_portal/reports.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['enrollment_by_program'] = (
            Enrollment.objects.filter(is_active=True)
            .values('program__name', 'program__level')
            .annotate(count=Count('id'))
            .order_by('-count')
        )
        _term_labels = dict(Payment.TERM_CHOICES)
        ctx['payments_by_term'] = [
            {**row, 'term_display': _term_labels.get(row['term'], row['term'])}
            for row in (
                Payment.objects.values('term', 'academic_year')
                .annotate(total=Sum('amount'), count=Count('id'))
                .order_by('academic_year', 'term')
            )
        ]
        ctx['payments_by_program'] = (
            Payment.objects.values('enrollment__program__name')
            .annotate(total=Sum('amount'), count=Count('id'))
            .order_by('-total')
        )
        ctx['certificates_by_program'] = (
            Certificate.objects.values('enrollment__program__name')
            .annotate(count=Count('id'))
            .order_by('-count')
        )
        ctx['total_payments'] = Payment.objects.aggregate(total=Sum('amount'))['total'] or 0
        ctx['total_certificates'] = Certificate.objects.count()
        ctx['total_active_students'] = Enrollment.objects.filter(is_active=True).count()
        ctx['total_active_programs'] = Program.objects.filter(is_active=True).count()
        return ctx


# ---------------------------------------------------------------------------
# Super Admin — Module Permission Management
# ---------------------------------------------------------------------------

class AdminPermissionsListView(SuperAdminRequiredMixin, View):
    template_name = 'admin_portal/permissions/list.html'

    def get(self, request):
        from django.contrib.auth import get_user_model
        User = get_user_model()
        admin_users = User.objects.filter(role=User.ADMIN).order_by('first_name', 'last_name')
        # For each admin, check if they have any restrictions configured
        user_data = []
        for u in admin_users:
            perm_count = AdminModulePermission.objects.filter(user=u).count()
            user_data.append({
                'user': u,
                'is_restricted': perm_count > 0,
                'perm_count': perm_count,
            })
        return render(request, self.template_name, {
            'user_data': user_data,
            'total_modules': len(MODULES),
        })


class AdminPermissionsEditView(SuperAdminRequiredMixin, View):
    template_name = 'admin_portal/permissions/edit.html'

    def _get_admin(self, pk):
        from django.contrib.auth import get_user_model
        User = get_user_model()
        return get_object_or_404(User, pk=pk, role=User.ADMIN)

    def get(self, request, pk):
        admin_user = self._get_admin(pk)
        existing = {p.module: p for p in AdminModulePermission.objects.filter(user=admin_user)}
        rows = []
        for module_key, module_label in MODULES:
            p = existing.get(module_key)
            rows.append({
                'key': module_key,
                'label': module_label,
                'can_view':   p.can_view   if p else True,
                'can_edit':   p.can_edit   if p else True,
                'can_delete': p.can_delete if p else True,
                'restricted': p is not None,
            })
        return render(request, self.template_name, {
            'admin_user': admin_user,
            'rows': rows,
            'is_restricted': bool(existing),
        })

    def post(self, request, pk):
        admin_user = self._get_admin(pk)
        enable = request.POST.get('enable_restrictions') == '1'

        if not enable:
            # Remove all restrictions → full access restored
            AdminModulePermission.objects.filter(user=admin_user).delete()
            messages.success(request, f'All restrictions removed for {admin_user.get_full_name()}. Full access restored.')
        else:
            for module_key, _ in MODULES:
                can_view   = request.POST.get(f'{module_key}_view')   == '1'
                can_edit   = request.POST.get(f'{module_key}_edit')   == '1'
                can_delete = request.POST.get(f'{module_key}_delete') == '1'
                AdminModulePermission.objects.update_or_create(
                    user=admin_user,
                    module=module_key,
                    defaults={
                        'can_view':   can_view,
                        'can_edit':   can_edit,
                        'can_delete': can_delete,
                    },
                )
            messages.success(request, f'Permissions updated for {admin_user.get_full_name()}.')

        return redirect('portal:admin_permissions')
