"""
Views for certifications — public catalog, proctoring, exam, payment, verification.
"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Prefetch, Q
from django.http import HttpResponse, HttpResponseBadRequest
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import DetailView, ListView

from apps.certifications.forms import (
    CertificateVerifyForm,
    CertificationProgramForm,
    CertPaymentProofForm,
    CreateProctorSessionForm,
    ExamQuestionForm,
    MCQBulkUploadForm,
    ProctorUnlockForm,
    TestScheduleSlotForm,
    TestingCenterForm,
)
from apps.certifications.models import (
    APP_AWAITING_PROCTOR,
    APP_PAID,
    APP_PENDING_PAYMENT,
    APP_PENDING_REATTEMPT,
    Certificate,
    CertificationApplication,
    CertificationProgram,
    ExamAttempt,
    ExamQuestion,
    ProctorSession,
    TestScheduleSlot,
    TestingCenter,
)
from apps.certifications.services import (
    apply_for_certification,
    begin_timed_exam,
    book_center_slot,
    build_mcq_csv_sample,
    cancel_exam_attempt,
    create_cert_stripe_checkout,
    create_proctor_session,
    finalize_cert_stripe_payment,
    import_mcq_csv,
    mark_application_paid,
    reject_application_payment,
    select_remote_exam,
    start_exam_attempt,
    submit_exam_attempt,
    unlock_with_proctor_code,
    verify_certificate_code,
)
from apps.dashboard.models import SiteSettings
from utils.permissions import (
    AdminOrAccountsStaffMixin,
    AdminOrSupportStaffMixin,
    AdminRequiredMixin,
    CertificateStaffMixin,
    ProctorStaffMixin,
    StudentRequiredMixin,
)


def _can_proctor_application(user, application):
    """Admin/support see all; teachers only programs they are assigned to."""
    if user.role in ('admin', 'support_staff'):
        return True
    if user.role == 'teacher':
        return application.program.teachers.filter(pk=user.pk).exists()
    return False


def _proctor_applications_qs(user):
    qs = CertificationApplication.objects.filter(
        status__in=['paid', 'awaiting_proctor', 'in_exam'],
    ).select_related('candidate', 'program')
    if user.role == 'teacher':
        qs = qs.filter(program__teachers=user)
    return qs.order_by('-updated_at')


# ── Public ──────────────────────────────────────────────────────────────────

class CertificationListView(ListView):
    template_name = 'certifications/program_list.html'
    context_object_name = 'programs'

    def get_queryset(self):
        from apps.courses.models import CATEGORY_BOTH, CATEGORY_CERT, Category
        qs = CertificationProgram.objects.filter(is_published=True).select_related('category')
        cat = self.request.GET.get('category', '').strip()
        if cat:
            qs = qs.filter(category__slug=cat)
        return qs

    def get_context_data(self, **kwargs):
        from apps.courses.models import CATEGORY_BOTH, CATEGORY_CERT, Category
        ctx = super().get_context_data(**kwargs)
        ctx['categories'] = Category.objects.filter(
            is_active=True,
            applies_to__in=[CATEGORY_CERT, CATEGORY_BOTH],
        )
        ctx['selected_category'] = self.request.GET.get('category', '').strip()
        return ctx


class CertificationDetailView(DetailView):
    template_name = 'certifications/program_detail.html'
    context_object_name = 'program'
    slug_url_kwarg = 'slug'

    def get_queryset(self):
        return CertificationProgram.objects.filter(is_published=True)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        user = self.request.user
        ctx['application'] = None
        if user.is_authenticated:
            ctx['application'] = CertificationApplication.objects.filter(
                candidate=user, program=self.object
            ).first()
        return ctx


class CertificateVerifyView(View):
    template_name = 'certifications/verify.html'

    def get(self, request):
        code = request.GET.get('code', '').strip()
        form = CertificateVerifyForm(initial={'code': code} if code else None)
        certificate = verify_certificate_code(code) if code else None
        return render(request, self.template_name, {
            'form': form,
            'certificate': certificate,
            'searched': bool(code),
            'is_fake': bool(code) and certificate is None,
        })

    def post(self, request):
        form = CertificateVerifyForm(request.POST)
        certificate = None
        searched = True
        is_fake = False
        if form.is_valid():
            certificate = verify_certificate_code(form.cleaned_data['code'])
            if not certificate:
                is_fake = True
        return render(request, self.template_name, {
            'form': form,
            'certificate': certificate,
            'searched': searched,
            'is_fake': is_fake,
        })


def _can_print_hard_certificate(user):
    return bool(
        user.is_authenticated
        and getattr(user, 'role', None) in ('admin', 'support_staff')
    )


def _can_access_certificate(user, certificate):
    if not user.is_authenticated:
        return False
    if _can_print_hard_certificate(user):
        return True
    return certificate.application.candidate_id == user.id


class CertificateDetailsView(LoginRequiredMixin, View):
    """Student/staff view of score + certificate metadata (no hard-copy print for students)."""
    template_name = 'certifications/certificate_details.html'

    def get(self, request, code):
        certificate = get_object_or_404(
            Certificate.objects.select_related('application__candidate', 'application__program'),
            verification_code__iexact=code,
        )
        if not _can_access_certificate(request.user, certificate):
            messages.error(request, 'You do not have access to this certificate.')
            return redirect('certifications:my_certs')
        site = SiteSettings.load()
        if _can_print_hard_certificate(request.user):
            back_url = reverse('certifications:admin_certificates')
        else:
            back_url = reverse('certifications:my_certs')
        return render(request, self.template_name, {
            'certificate': certificate,
            'verify_url': site.get_certificate_verify_url(request),
            'can_print_hard': _can_print_hard_certificate(request.user),
            'back_url': back_url,
        })


class CertificateSoftCopyView(View):
    """
    Soft-copy certificate (watermarked).
    Owner or staff can open; public visitors with the code also see soft copy only.
    """
    template_name = 'certifications/certificate_document.html'

    def get(self, request, code):
        certificate = get_object_or_404(
            Certificate.objects.select_related('application__candidate', 'application__program'),
            verification_code__iexact=code,
        )
        site = SiteSettings.load()
        back_url = reverse('certifications:verify')
        if request.user.is_authenticated and _can_access_certificate(request.user, certificate):
            back_url = reverse('certifications:certificate_details', kwargs={'code': certificate.verification_code})
        return render(request, self.template_name, {
            'certificate': certificate,
            'soft_copy': True,
            'verify_url': site.get_certificate_verify_url(request),
            'back_url': back_url,
            'auto_print': request.GET.get('print') == '1',
        })


class CertificateHardCopyView(CertificateStaffMixin, View):
    """Official hard-copy certificate — print only for admin / support staff."""
    template_name = 'certifications/certificate_document.html'

    def get(self, request, code):
        certificate = get_object_or_404(
            Certificate.objects.select_related('application__candidate', 'application__program'),
            verification_code__iexact=code,
        )
        site = SiteSettings.load()
        return render(request, self.template_name, {
            'certificate': certificate,
            'soft_copy': False,
            'verify_url': site.get_certificate_verify_url(request),
            'back_url': reverse('certifications:admin_certificates'),
            'auto_print': request.GET.get('print') == '1',
        })


class CertificatePublicView(View):
    """Legacy public URL — always serve soft copy (never a clean hard copy)."""

    def get(self, request, code):
        return redirect('certifications:certificate_soft', code=code)


# ── Student flow ────────────────────────────────────────────────────────────

class ApplyCertificationView(StudentRequiredMixin, View):
    """Confirm education background, then create certification application."""
    template_name = 'certifications/apply.html'

    def get(self, request, slug):
        from apps.accounts.forms import EducationBackgroundForm
        program = get_object_or_404(CertificationProgram, slug=slug, is_published=True)
        form = EducationBackgroundForm(instance=request.user)
        return render(request, self.template_name, {'program': program, 'form': form})

    def post(self, request, slug):
        from apps.accounts.forms import EducationBackgroundForm
        program = get_object_or_404(CertificationProgram, slug=slug, is_published=True)
        form = EducationBackgroundForm(request.POST, instance=request.user)
        if not form.is_valid():
            return render(request, self.template_name, {'program': program, 'form': form})
        form.save()

        try:
            app = apply_for_certification(request.user, program)
        except ValueError as exc:
            messages.warning(request, str(exc))
            return redirect('certifications:program_detail', slug=slug)

        if app.status == APP_PENDING_PAYMENT:
            messages.success(request, 'Application created. Please complete the certification fee.')
            return redirect('certifications:pay', pk=app.pk)

        messages.success(request, 'Payment done — schedule your test (testing center or remote).')
        return redirect('certifications:schedule', pk=app.pk)


class MyCertificationsView(LoginRequiredMixin, ListView):
    template_name = 'certifications/my_certs.html'
    context_object_name = 'applications'

    def get_queryset(self):
        return (
            CertificationApplication.objects
            .filter(candidate=self.request.user)
            .select_related('program', 'testing_center', 'schedule_slot', 'certificate')
            .prefetch_related(
                Prefetch(
                    'attempts',
                    queryset=ExamAttempt.objects.order_by('-started_at'),
                )
            )
            .order_by('-created_at')
        )

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['certificates'] = Certificate.objects.filter(
            application__candidate=self.request.user,
            is_revoked=False,
        ).select_related('application__program')
        return ctx


class CertPayView(StudentRequiredMixin, View):
    template_name = 'certifications/pay.html'

    def get_app(self, request, pk):
        return get_object_or_404(
            CertificationApplication,
            pk=pk,
            candidate=request.user,
        )

    def _payable(self, app):
        return app.status in (APP_PENDING_PAYMENT, APP_PENDING_REATTEMPT)

    def get(self, request, pk):
        app = self.get_app(request, pk)
        if not self._payable(app):
            messages.info(request, 'No payment is due for this application right now.')
            return redirect('certifications:my_certs')
        site = SiteSettings.load()
        kind = 'reattempt' if app.status == APP_PENDING_REATTEMPT else 'initial'
        from apps.promotions.services import cert_payable
        return render(request, self.template_name, {
            'application': app,
            'program': app.program,
            'form': CertPaymentProofForm(instance=app),
            'manual_enabled': site.enable_manual_payment,
            'stripe_enabled': site.stripe_ready,
            'site': site,
            'payment_kind': kind,
            'list_price': app.current_fee_due(),
            'amount_due': cert_payable(app),
        })

    def post(self, request, pk):
        app = self.get_app(request, pk)
        if not self._payable(app):
            return redirect('certifications:my_certs')
        site = SiteSettings.load()
        if not site.enable_manual_payment:
            messages.error(request, 'Manual payment is disabled.')
            return redirect('certifications:pay', pk=pk)
        form = CertPaymentProofForm(request.POST, request.FILES, instance=app)
        if form.is_valid():
            obj = form.save(commit=False)
            obj.payment_method = 'manual'
            obj.payment_kind = 'reattempt' if app.status == APP_PENDING_REATTEMPT else 'initial'
            obj.payment_rejection_reason = ''
            obj.save()
            messages.success(request, 'Payment proof submitted. Staff will review shortly.')
            return redirect('certifications:my_certs')
        kind = 'reattempt' if app.status == APP_PENDING_REATTEMPT else 'initial'
        from apps.promotions.services import cert_payable
        return render(request, self.template_name, {
            'application': app,
            'program': app.program,
            'form': form,
            'manual_enabled': site.enable_manual_payment,
            'stripe_enabled': site.stripe_ready,
            'site': site,
            'payment_kind': kind,
            'list_price': app.current_fee_due(),
            'amount_due': cert_payable(app),
        })


class ApplyCertPromoView(StudentRequiredMixin, View):
    def post(self, request, pk):
        from apps.promotions.services import (
            PromoError, apply_promo_to_cert_application, cert_payable,
        )
        from apps.certifications.services import mark_application_paid
        app = get_object_or_404(CertificationApplication, pk=pk, candidate=request.user)
        if app.status not in (APP_PENDING_PAYMENT, APP_PENDING_REATTEMPT):
            return redirect('certifications:my_certs')
        try:
            apply_promo_to_cert_application(app, request.POST.get('code', ''))
            app.refresh_from_db()
            due = cert_payable(app)
            kind = 'reattempt' if app.status == APP_PENDING_REATTEMPT else 'initial'
            if due <= 0:
                mark_application_paid(app, method='free', kind=kind)
                messages.success(request, 'Promo applied — fee is fully covered. You can schedule your test.')
                return redirect('certifications:schedule', pk=app.pk)
            messages.success(
                request,
                f'Promo {app.promo_code.code} applied for {app.promo_code.institute_name}.',
            )
        except PromoError as exc:
            messages.error(request, str(exc))
        return redirect('certifications:pay', pk=pk)


class ClearCertPromoView(StudentRequiredMixin, View):
    def post(self, request, pk):
        from apps.promotions.services import clear_promo_from_cert_application
        app = get_object_or_404(CertificationApplication, pk=pk, candidate=request.user)
        clear_promo_from_cert_application(app)
        messages.info(request, 'Promo code removed.')
        return redirect('certifications:pay', pk=pk)


class CertStripeCheckoutView(StudentRequiredMixin, View):
    def post(self, request, pk):
        app = get_object_or_404(CertificationApplication, pk=pk, candidate=request.user)
        if app.status not in (APP_PENDING_PAYMENT, APP_PENDING_REATTEMPT):
            return redirect('certifications:my_certs')
        kind = 'reattempt' if app.status == APP_PENDING_REATTEMPT else 'initial'
        try:
            session = create_cert_stripe_checkout(app, request, kind=kind)
            if hasattr(session, 'url'):
                return redirect(session.url)
            messages.success(request, 'Payment recorded.')
            return redirect('certifications:my_certs')
        except Exception as exc:
            messages.error(request, f'Stripe error: {exc}')
            return redirect('certifications:pay', pk=pk)


class CertStripeSuccessView(StudentRequiredMixin, View):
    def get(self, request, pk):
        app = get_object_or_404(CertificationApplication, pk=pk, candidate=request.user)
        session_id = request.GET.get('session_id', '').strip()
        if not session_id:
            messages.error(request, 'Missing Stripe session.')
            return redirect('certifications:pay', pk=pk)
        import stripe
        site = SiteSettings.load()
        stripe.api_key = site.stripe_secret_key
        try:
            session = stripe.checkout.Session.retrieve(session_id)
        except Exception as exc:
            messages.error(request, f'Could not verify payment: {exc}')
            return redirect('certifications:pay', pk=pk)
        if session.get('payment_status') == 'paid' or session.get('status') == 'complete':
            finalize_cert_stripe_payment(app, session)
            messages.success(
                request,
                'Payment successful! Next: schedule your test at a TSN Digital Center or choose remote.',
            )
            return redirect('certifications:schedule', pk=app.pk)
        messages.warning(request, 'Payment not complete yet.')
        return redirect('certifications:pay', pk=pk)


@method_decorator(csrf_exempt, name='dispatch')
class CertStripeWebhookView(View):
    def post(self, request):
        import json
        import stripe
        site = SiteSettings.load()
        payload = request.body
        sig = request.META.get('HTTP_STRIPE_SIGNATURE', '')
        if site.stripe_webhook_secret:
            try:
                event = stripe.Webhook.construct_event(payload, sig, site.stripe_webhook_secret)
            except Exception:
                return HttpResponseBadRequest('Invalid signature')
        else:
            try:
                event = json.loads(payload)
            except json.JSONDecodeError:
                return HttpResponseBadRequest('Invalid JSON')

        event_type = event['type'] if isinstance(event, dict) else event.type
        data_object = event['data']['object'] if isinstance(event, dict) else event.data.object
        if event_type == 'checkout.session.completed':
            meta = data_object.get('metadata') or {}
            if meta.get('type') == 'certification' and meta.get('cert_application_id'):
                try:
                    app = CertificationApplication.objects.get(pk=meta['cert_application_id'])
                    finalize_cert_stripe_payment(app, data_object)
                except CertificationApplication.DoesNotExist:
                    pass
        return HttpResponse(status=200)


class UnlockExamView(StudentRequiredMixin, View):
    """Student joins teacher meeting, then enters the live unlock code."""
    template_name = 'certifications/unlock_exam.html'

    def get_app(self, request, pk):
        return get_object_or_404(
            CertificationApplication.objects.select_related('program'),
            pk=pk,
            candidate=request.user,
        )

    def get(self, request, pk):
        app = self.get_app(request, pk)
        if app.needs_schedule:
            return redirect('certifications:schedule', pk=app.pk)
        if app.status == APP_AWAITING_PROCTOR and app.can_take_exam:
            return redirect('certifications:start_exam_page', pk=app.pk)
        if not app.can_enter_proctor_code and app.status != 'in_exam':
            if app.status == APP_PENDING_REATTEMPT:
                messages.info(request, 'Pay the reattempt fee before unlocking another exam.')
                return redirect('certifications:pay', pk=app.pk)
            messages.info(request, 'Complete payment and schedule first.')
            return redirect('certifications:my_certs')
        latest = (
            ProctorSession.objects.filter(application=app)
            .order_by('-created_at')
            .first()
        )
        return render(request, self.template_name, {
            'application': app,
            'program': app.program,
            'form': ProctorUnlockForm(),
            'latest_session': latest,
        })

    def post(self, request, pk):
        app = self.get_app(request, pk)
        form = ProctorUnlockForm(request.POST)
        if form.is_valid():
            try:
                unlock_with_proctor_code(app, form.cleaned_data['code'])
                messages.success(request, 'Exam unlocked. You may start the test now.')
                return redirect('certifications:start_exam_page', pk=app.pk)
            except ValueError as exc:
                messages.error(request, str(exc))
        return render(request, self.template_name, {
            'application': app,
            'program': app.program,
            'form': form,
            'latest_session': None,
        })


class StartExamPageView(StudentRequiredMixin, View):
    """Confirm start after proctor unlock (GET shows button; POST starts)."""
    template_name = 'certifications/start_exam.html'

    def get(self, request, pk):
        app = get_object_or_404(
            CertificationApplication.objects.select_related('program'),
            pk=pk,
            candidate=request.user,
        )
        open_attempt = ExamAttempt.objects.filter(
            application=app, submitted_at__isnull=True
        ).first()
        if open_attempt and (open_attempt.is_practice_phase or not open_attempt.time_expired()):
            return redirect('certifications:exam_launch', attempt_id=open_attempt.pk)
        if not app.can_take_exam and not (app.program.require_proctor is False and app.status == 'paid'):
            if app.can_enter_proctor_code:
                return redirect('certifications:unlock_exam', pk=app.pk)
            messages.warning(request, 'You cannot start the exam yet.')
            return redirect('certifications:my_certs')
        return render(request, self.template_name, {
            'application': app,
            'program': app.program,
        })

    def post(self, request, pk):
        app = get_object_or_404(CertificationApplication, pk=pk, candidate=request.user)
        try:
            attempt = start_exam_attempt(app)
        except ValueError as exc:
            messages.error(request, str(exc))
            if app.can_enter_proctor_code:
                return redirect('certifications:unlock_exam', pk=app.pk)
            return redirect('certifications:my_certs')
        return redirect('certifications:exam_launch', attempt_id=attempt.pk)


class StartExamView(StudentRequiredMixin, View):
    def post(self, request, pk):
        return StartExamPageView.as_view()(request, pk=pk)


class ExamLaunchView(StudentRequiredMixin, View):
    """Opens the dedicated exam window from the dashboard."""
    template_name = 'certifications/exam_launch.html'

    def get(self, request, attempt_id):
        attempt = get_object_or_404(
            ExamAttempt.objects.select_related('application__program'),
            pk=attempt_id,
            application__candidate=request.user,
        )
        if attempt.submitted_at:
            return redirect('certifications:exam_result', attempt_id=attempt.pk)
        if attempt.timer_started_at and attempt.time_expired():
            submit_exam_attempt(attempt, {})
            return redirect('certifications:exam_result', attempt_id=attempt.pk)
        return render(request, self.template_name, {
            'attempt': attempt,
            'program': attempt.application.program,
            'take_exam_url': request.build_absolute_uri(
                reverse('certifications:take_exam', args=[attempt.pk])
            ),
        })


class TakeExamView(StudentRequiredMixin, View):
    template_name = 'certifications/take_exam.html'

    def get_attempt(self, request, attempt_id):
        return get_object_or_404(
            ExamAttempt.objects.select_related('application__program'),
            pk=attempt_id,
            application__candidate=request.user,
        )

    def get(self, request, attempt_id):
        attempt = self.get_attempt(request, attempt_id)
        if attempt.submitted_at:
            return redirect('certifications:exam_result', attempt_id=attempt.pk)
        # Timer only applies after practice is done
        if attempt.timer_started_at and attempt.time_expired():
            submit_exam_attempt(attempt, {})
            messages.warning(request, 'Time expired — your exam was submitted automatically.')
            return redirect('certifications:exam_result', attempt_id=attempt.pk)

        questions = (
            ExamQuestion.objects.filter(pk__in=attempt.question_ids)
            .prefetch_related('choices')
        )
        qmap = {q.pk: q for q in questions}
        ordered = [qmap[qid] for qid in attempt.question_ids if qid in qmap]
        return render(request, self.template_name, {
            'attempt': attempt,
            'program': attempt.application.program,
            'questions': ordered,
            'is_practice': attempt.is_practice_phase,
            'seconds_remaining': attempt.seconds_remaining,
            'deadline_iso': (
                attempt.deadline_at.isoformat() if attempt.timer_started_at else ''
            ),
            'time_limit_minutes': (
                attempt.application.program.time_limit_minutes
                or attempt.application.program.recommended_time_limit_minutes
            ),
        })

    def post(self, request, attempt_id):
        attempt = self.get_attempt(request, attempt_id)
        if attempt.submitted_at:
            return redirect('certifications:exam_result', attempt_id=attempt.pk)
        if attempt.is_practice_phase:
            messages.warning(
                request,
                'Complete the practice question first, then start the real exam.',
            )
            return redirect('certifications:take_exam', attempt_id=attempt.pk)
        answers = {}
        for key, val in request.POST.items():
            if key.startswith('q_'):
                answers[key[2:]] = val
        try:
            submit_exam_attempt(attempt, answers)
        except ValueError as exc:
            messages.error(request, str(exc))
            return redirect('certifications:my_certs')
        return redirect('certifications:exam_result', attempt_id=attempt.pk)


class BeginTimedExamView(StudentRequiredMixin, View):
    """POST after practice question — starts the real exam timer."""

    def post(self, request, attempt_id):
        attempt = get_object_or_404(
            ExamAttempt.objects.select_related('application'),
            pk=attempt_id,
            application__candidate=request.user,
        )
        if attempt.submitted_at:
            return redirect('certifications:exam_result', attempt_id=attempt.pk)
        try:
            begin_timed_exam(attempt)
        except ValueError as exc:
            messages.error(request, str(exc))
            return redirect('certifications:my_certs')
        return redirect('certifications:take_exam', attempt_id=attempt.pk)


class CancelExamView(StudentRequiredMixin, View):
    """Cancel timed exam when candidate leaves the tab/browser."""

    def post(self, request, attempt_id):
        attempt = get_object_or_404(
            ExamAttempt.objects.select_related('application__program'),
            pk=attempt_id,
            application__candidate=request.user,
        )
        if attempt.submitted_at:
            return redirect('certifications:exam_result', attempt_id=attempt.pk)

        answers = {}
        for key, val in request.POST.items():
            if key.startswith('q_'):
                answers[key[2:]] = val
        reason = (request.POST.get('reason') or 'tab_leave')[:64]

        try:
            cancel_exam_attempt(attempt, reason=reason, answers_map=answers)
        except ValueError as exc:
            messages.error(request, str(exc))
            return redirect('certifications:my_certs')

        attempt.refresh_from_db()
        if attempt.was_cancelled:
            messages.error(
                request,
                'Exam cancelled — you left the exam tab or browser. '
                'This attempt has been forfeited.',
            )
        return redirect('certifications:exam_result', attempt_id=attempt.pk)


class ExamResultView(StudentRequiredMixin, DetailView):
    template_name = 'certifications/exam_result.html'
    context_object_name = 'attempt'
    pk_url_kwarg = 'attempt_id'

    def get_queryset(self):
        return ExamAttempt.objects.filter(
            application__candidate=self.request.user,
            submitted_at__isnull=False,
        ).select_related('application__program', 'application__certificate')

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        attempt = ctx['attempt']
        ctx['results_ready'] = attempt.results_ready
        ctx['results_seconds_remaining'] = attempt.results_seconds_remaining
        ctx['results_available_at'] = attempt.results_available_at
        ctx['results_delay_minutes'] = (
            attempt.application.program.results_delay_minutes
            if attempt.application.program.results_delay_minutes is not None
            else 5
        )
        return ctx


# ── Teacher / staff proctoring ───────────────────────────────────────────────

class ProctorQueueView(ProctorStaffMixin, ListView):
    """Candidates who paid and need a live unlock code (scoped by assigned teachers)."""
    template_name = 'certifications/proctor_queue.html'
    context_object_name = 'applications'

    def get_queryset(self):
        return _proctor_applications_qs(self.request.user)


class CreateProctorSessionView(ProctorStaffMixin, View):
    template_name = 'certifications/proctor_create.html'

    def get(self, request, pk):
        app = get_object_or_404(
            CertificationApplication.objects.select_related('candidate', 'program'),
            pk=pk,
        )
        if not _can_proctor_application(request.user, app):
            messages.error(request, 'You are not assigned to this certification program.')
            return redirect('certifications:proctor_queue')
        return render(request, self.template_name, {
            'application': app,
            'program': app.program,
            'form': CreateProctorSessionForm(),
            'active_sessions': app.proctor_sessions.filter(
                is_revoked=False, unlocked_at__isnull=True
            )[:5],
        })

    def post(self, request, pk):
        app = get_object_or_404(
            CertificationApplication.objects.select_related('candidate', 'program'),
            pk=pk,
        )
        if not _can_proctor_application(request.user, app):
            messages.error(request, 'You are not assigned to this certification program.')
            return redirect('certifications:proctor_queue')
        form = CreateProctorSessionForm(request.POST)
        if form.is_valid():
            try:
                session = create_proctor_session(
                    app,
                    created_by=request.user,
                    meeting_url=form.cleaned_data.get('meeting_url') or '',
                    notes=form.cleaned_data.get('notes') or '',
                    hours_valid=form.cleaned_data['hours_valid'],
                )
                messages.success(
                    request,
                    f'Unlock code created: {session.code} — share it with the candidate in the live meeting.',
                )
                return redirect('certifications:proctor_session_detail', pk=session.pk)
            except ValueError as exc:
                messages.error(request, str(exc))
        return render(request, self.template_name, {
            'application': app,
            'program': app.program,
            'form': form,
            'active_sessions': app.proctor_sessions.filter(
                is_revoked=False, unlocked_at__isnull=True
            )[:5],
        })


class ProctorSessionDetailView(ProctorStaffMixin, DetailView):
    model = ProctorSession
    template_name = 'certifications/proctor_session.html'
    context_object_name = 'session'

    def get_queryset(self):
        qs = ProctorSession.objects.select_related(
            'application__candidate', 'application__program', 'created_by'
        )
        if self.request.user.role == 'teacher':
            qs = qs.filter(application__program__teachers=self.request.user)
        return qs


# ── Admin / staff ───────────────────────────────────────────────────────────

class AdminProgramListView(AdminRequiredMixin, ListView):
    template_name = 'certifications/admin_program_list.html'
    context_object_name = 'programs'
    queryset = CertificationProgram.objects.all()


class AdminProgramCreateView(AdminRequiredMixin, View):
    template_name = 'certifications/admin_program_form.html'

    def get(self, request):
        return render(request, self.template_name, {
            'form': CertificationProgramForm(),
            'action': 'Create Certification',
        })

    def post(self, request):
        form = CertificationProgramForm(request.POST, request.FILES)
        if form.is_valid():
            program = form.save()
            messages.success(request, f'Certification "{program.title}" created.')
            return redirect('certifications:admin_questions', pk=program.pk)
        return render(request, self.template_name, {'form': form, 'action': 'Create Certification'})


class AdminProgramEditView(AdminRequiredMixin, View):
    template_name = 'certifications/admin_program_form.html'

    def get(self, request, pk):
        program = get_object_or_404(CertificationProgram, pk=pk)
        return render(request, self.template_name, {
            'form': CertificationProgramForm(instance=program),
            'program': program,
            'action': 'Edit Certification',
        })

    def post(self, request, pk):
        program = get_object_or_404(CertificationProgram, pk=pk)
        form = CertificationProgramForm(request.POST, request.FILES, instance=program)
        if form.is_valid():
            form.save()
            messages.success(request, 'Certification updated.')
            return redirect('certifications:admin_programs')
        return render(request, self.template_name, {
            'form': form, 'program': program, 'action': 'Edit Certification',
        })


class AdminQuestionListView(AdminRequiredMixin, View):
    template_name = 'certifications/admin_questions.html'

    def get(self, request, pk):
        program = get_object_or_404(CertificationProgram, pk=pk)
        questions = (
            program.questions.prefetch_related('choices')
            .order_by('-id')[:80]
        )
        return render(request, self.template_name, {
            'program': program,
            'questions': questions,
            'form': ExamQuestionForm(),
            'bulk_form': MCQBulkUploadForm(),
        })

    def post(self, request, pk):
        program = get_object_or_404(CertificationProgram, pk=pk)
        form = ExamQuestionForm(request.POST)
        if form.is_valid():
            form.save_with_choices(program)
            messages.success(request, 'Question added.')
            return redirect('certifications:admin_questions', pk=pk)
        questions = (
            program.questions.prefetch_related('choices')
            .order_by('-id')[:80]
        )
        return render(request, self.template_name, {
            'program': program,
            'questions': questions,
            'form': form,
            'bulk_form': MCQBulkUploadForm(),
        })


class AdminQuestionCSVSampleView(AdminRequiredMixin, View):
    """Download a sample CSV template for bulk MCQ upload."""

    def get(self, request, pk):
        get_object_or_404(CertificationProgram, pk=pk)
        content = build_mcq_csv_sample()
        response = HttpResponse(content, content_type='text/csv; charset=utf-8')
        response['Content-Disposition'] = (
            'attachment; filename="mcq_question_bank_sample.csv"'
        )
        return response


class AdminQuestionBulkUploadView(AdminRequiredMixin, View):
    """Bulk-import MCQs from a CSV file into a program's question bank."""

    def post(self, request, pk):
        program = get_object_or_404(CertificationProgram, pk=pk)
        form = MCQBulkUploadForm(request.POST, request.FILES)
        if not form.is_valid():
            for err in form.errors.get('csv_file', form.non_field_errors()):
                messages.error(request, err)
            return redirect('certifications:admin_questions', pk=pk)

        try:
            result = import_mcq_csv(program, form.cleaned_data['csv_file'])
        except ValueError as exc:
            messages.error(request, str(exc))
            return redirect('certifications:admin_questions', pk=pk)
        except UnicodeDecodeError:
            messages.error(request, 'Could not read the file. Please upload a UTF-8 CSV.')
            return redirect('certifications:admin_questions', pk=pk)

        created = result['created']
        err_count = result['error_count']
        if created:
            messages.success(
                request,
                f'Imported {created} MCQ{"s" if created != 1 else ""} into the question bank.',
            )
        else:
            messages.warning(request, 'No questions were imported.')

        if result['skipped']:
            messages.info(request, f'Skipped {result["skipped"]} empty row(s).')

        if err_count:
            preview = '; '.join(
                f'Row {e["row"]}: {e["message"]}' for e in result['errors'][:5]
            )
            extra = f' (+{err_count - 5} more)' if err_count > 5 else ''
            messages.warning(
                request,
                f'{err_count} row(s) had errors and were skipped. {preview}{extra}',
            )

        return redirect('certifications:admin_questions', pk=pk)


class AdminQuestionDeleteView(AdminRequiredMixin, View):
    def post(self, request, pk):
        question = get_object_or_404(ExamQuestion, pk=pk)
        program_id = question.program_id
        question.delete()
        messages.success(request, 'Question deleted.')
        return redirect('certifications:admin_questions', pk=program_id)


class AdminApplicationListView(AdminOrAccountsStaffMixin, ListView):
    template_name = 'certifications/admin_applications.html'
    context_object_name = 'applications'

    def get_queryset(self):
        qs = CertificationApplication.objects.select_related(
            'candidate', 'program', 'approved_by'
        ).order_by('-created_at')
        status = self.request.GET.get('status', '')
        if status:
            qs = qs.filter(status=status)
        if self.request.GET.get('needs_review') == '1':
            qs = qs.filter(
                status__in=[APP_PENDING_PAYMENT, APP_PENDING_REATTEMPT]
            ).filter(Q(payment_proof__isnull=False) & ~Q(payment_proof=''))
        return qs

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        base = CertificationApplication.objects.all()
        ctx['status_filter'] = self.request.GET.get('status', '')
        ctx['pending_count'] = base.filter(
            status__in=[APP_PENDING_PAYMENT, APP_PENDING_REATTEMPT]
        ).filter(Q(payment_proof__isnull=False) & ~Q(payment_proof='')).count()
        ctx['paid_count'] = base.filter(status='paid').count()
        ctx['certified_count'] = base.filter(status='certified').count()
        return ctx


class AdminApplicationDetailView(AdminOrAccountsStaffMixin, View):
    """Review manual payment proof — view screenshot, approve or reject."""
    template_name = 'certifications/admin_application_detail.html'

    def get(self, request, pk):
        app = get_object_or_404(
            CertificationApplication.objects.select_related('candidate', 'program', 'approved_by'),
            pk=pk,
        )
        return render(request, self.template_name, {
            'application': app,
            'program': app.program,
            'can_review': (
                app.status in (APP_PENDING_PAYMENT, APP_PENDING_REATTEMPT)
                and bool(app.payment_proof)
            ),
        })


class AdminApproveApplicationView(AdminOrAccountsStaffMixin, View):
    def post(self, request, pk):
        app = get_object_or_404(CertificationApplication, pk=pk)
        if app.status not in (APP_PENDING_PAYMENT, APP_PENDING_REATTEMPT):
            messages.warning(request, 'This application is not awaiting payment approval.')
            return redirect('certifications:admin_applications')
        method = (app.payment_method or 'manual').lower()
        if method != 'stripe' and not app.payment_proof:
            messages.error(
                request,
                'No payment screenshot attached. Ask the candidate to upload proof, '
                'or open the application to review before approving.',
            )
            return redirect('certifications:admin_application_detail', pk=app.pk)
        kind = 'reattempt' if app.status == APP_PENDING_REATTEMPT else (app.payment_kind or 'initial')
        mark_application_paid(
            app,
            method=app.payment_method or 'manual',
            approved_by=request.user,
            kind=kind,
        )
        app.payment_rejection_reason = ''
        app.save(update_fields=['payment_rejection_reason', 'updated_at'])
        messages.success(
            request,
            f'Payment approved for {app.candidate.get_display_name()}. '
            f'They can now schedule their test.',
        )
        return redirect('certifications:admin_applications')


class AdminRejectApplicationView(AdminOrAccountsStaffMixin, View):
    def post(self, request, pk):
        app = get_object_or_404(CertificationApplication, pk=pk)
        reason = (request.POST.get('rejection_reason') or '').strip()
        try:
            reject_application_payment(app, rejected_by=request.user, reason=reason)
        except ValueError as exc:
            messages.error(request, str(exc))
            return redirect('certifications:admin_application_detail', pk=app.pk)
        messages.warning(
            request,
            f'Payment proof rejected for {app.candidate.get_display_name()}. '
            f'They can upload a new screenshot.',
        )
        return redirect('certifications:admin_applications')


class AdminCertificateListView(CertificateStaffMixin, ListView):
    template_name = 'certifications/admin_certificates.html'
    context_object_name = 'certificates'
    queryset = Certificate.objects.select_related(
        'application__candidate', 'application__program'
    )


# ── Testing centers & schedules ──────────────────────────────────────────────

class TestingCenterListView(ListView):
    """Public list of TSN Digital testing centers."""
    template_name = 'certifications/center_list.html'
    context_object_name = 'centers'

    def get_queryset(self):
        return TestingCenter.objects.filter(is_active=True)


class ScheduleExamView(StudentRequiredMixin, View):
    """After payment: book a testing center slot or choose remote online."""
    template_name = 'certifications/schedule_exam.html'

    def get_app(self, request, pk):
        return get_object_or_404(
            CertificationApplication.objects.select_related(
                'program', 'testing_center', 'schedule_slot'
            ),
            pk=pk,
            candidate=request.user,
        )

    def get(self, request, pk):
        from django.db.models import Q
        from django.utils import timezone as tz

        app = self.get_app(request, pk)
        if app.status != APP_PAID and not app.needs_schedule:
            if app.can_enter_proctor_code:
                return redirect('certifications:unlock_exam', pk=app.pk)
            return redirect('certifications:my_certs')
        if app.status != APP_PAID:
            messages.info(request, 'Complete payment before scheduling.')
            return redirect('certifications:pay', pk=app.pk)

        today = tz.localdate()
        slots = (
            TestScheduleSlot.objects.filter(
                is_active=True,
                date__gte=today,
                center__is_active=True,
            )
            .filter(Q(program__isnull=True) | Q(program=app.program))
            .select_related('center', 'program')
            .order_by('date', 'start_time')
        )
        bookable = [s for s in slots if s.is_bookable or app.schedule_slot_id == s.pk]
        centers = TestingCenter.objects.filter(is_active=True)
        return render(request, self.template_name, {
            'application': app,
            'program': app.program,
            'centers': centers,
            'slots': bookable,
        })

    def post(self, request, pk):
        app = self.get_app(request, pk)
        if app.status != APP_PAID:
            return redirect('certifications:my_certs')
        mode = request.POST.get('exam_mode', '').strip()
        try:
            if mode == 'remote':
                select_remote_exam(app)
                messages.success(request, 'Remote exam selected. Join the live meeting to unlock.')
                return redirect('certifications:unlock_exam', pk=app.pk)
            if mode == 'center':
                slot_id = request.POST.get('slot_id')
                slot = get_object_or_404(TestScheduleSlot, pk=slot_id)
                book_center_slot(app, slot)
                messages.success(
                    request,
                    f'Booked at {slot.center.name} on {slot.label}. '
                    f'Arrive on time; staff will give you the unlock code.',
                )
                return redirect('certifications:unlock_exam', pk=app.pk)
            messages.error(request, 'Choose a testing center slot or remote exam.')
        except ValueError as exc:
            messages.error(request, str(exc))
        return redirect('certifications:schedule', pk=app.pk)


class AdminCenterListView(AdminRequiredMixin, ListView):
    template_name = 'certifications/admin_centers.html'
    context_object_name = 'centers'
    queryset = TestingCenter.objects.all()


class AdminCenterCreateView(AdminRequiredMixin, View):
    template_name = 'certifications/admin_center_form.html'

    def get(self, request):
        return render(request, self.template_name, {
            'form': TestingCenterForm(),
            'action': 'Add testing center',
        })

    def post(self, request):
        form = TestingCenterForm(request.POST)
        if form.is_valid():
            center = form.save()
            messages.success(request, f'Center "{center.name}" created.')
            return redirect('certifications:admin_centers')
        return render(request, self.template_name, {'form': form, 'action': 'Add testing center'})


class AdminCenterEditView(AdminRequiredMixin, View):
    template_name = 'certifications/admin_center_form.html'

    def get(self, request, pk):
        center = get_object_or_404(TestingCenter, pk=pk)
        return render(request, self.template_name, {
            'form': TestingCenterForm(instance=center),
            'center': center,
            'action': 'Edit testing center',
        })

    def post(self, request, pk):
        center = get_object_or_404(TestingCenter, pk=pk)
        form = TestingCenterForm(request.POST, instance=center)
        if form.is_valid():
            form.save()
            messages.success(request, 'Testing center updated.')
            return redirect('certifications:admin_centers')
        return render(request, self.template_name, {
            'form': form, 'center': center, 'action': 'Edit testing center',
        })


class AdminScheduleListView(AdminOrSupportStaffMixin, ListView):
    template_name = 'certifications/admin_schedules.html'
    context_object_name = 'slots'

    def get_queryset(self):
        from django.utils import timezone as tz
        qs = TestScheduleSlot.objects.select_related('center', 'program').order_by('date', 'start_time')
        center = self.request.GET.get('center')
        if center:
            qs = qs.filter(center_id=center)
        if self.request.GET.get('upcoming') == '1':
            qs = qs.filter(date__gte=tz.localdate())
        return qs

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx['centers'] = TestingCenter.objects.filter(is_active=True)
        ctx['center_filter'] = self.request.GET.get('center', '')
        return ctx


class AdminScheduleCreateView(AdminOrSupportStaffMixin, View):
    template_name = 'certifications/admin_schedule_form.html'

    def get(self, request):
        initial = {}
        if request.GET.get('center'):
            initial['center'] = request.GET.get('center')
        return render(request, self.template_name, {
            'form': TestScheduleSlotForm(initial=initial),
            'action': 'Add schedule slot',
        })

    def post(self, request):
        form = TestScheduleSlotForm(request.POST)
        if form.is_valid():
            slot = form.save()
            messages.success(request, f'Schedule slot created: {slot}')
            return redirect('certifications:admin_schedules')
        return render(request, self.template_name, {'form': form, 'action': 'Add schedule slot'})


class AdminScheduleEditView(AdminOrSupportStaffMixin, View):
    template_name = 'certifications/admin_schedule_form.html'

    def get(self, request, pk):
        slot = get_object_or_404(TestScheduleSlot, pk=pk)
        return render(request, self.template_name, {
            'form': TestScheduleSlotForm(instance=slot),
            'slot': slot,
            'action': 'Edit schedule slot',
        })

    def post(self, request, pk):
        slot = get_object_or_404(TestScheduleSlot, pk=pk)
        form = TestScheduleSlotForm(request.POST, instance=slot)
        if form.is_valid():
            form.save()
            messages.success(request, 'Schedule slot updated.')
            return redirect('certifications:admin_schedules')
        return render(request, self.template_name, {
            'form': form, 'slot': slot, 'action': 'Edit schedule slot',
        })


class AdminScheduleDeleteView(AdminRequiredMixin, View):
    def post(self, request, pk):
        slot = get_object_or_404(TestScheduleSlot, pk=pk)
        if slot.bookings.exists():
            messages.error(request, 'Cannot delete a slot that has bookings. Deactivate it instead.')
            return redirect('certifications:admin_schedules')
        slot.delete()
        messages.success(request, 'Schedule slot deleted.')
        return redirect('certifications:admin_schedules')


class AdminBookingsListView(AdminOrSupportStaffMixin, ListView):
    template_name = 'certifications/admin_bookings.html'
    context_object_name = 'applications'

    def get_queryset(self):
        return (
            CertificationApplication.objects.filter(schedule_slot__isnull=False)
            .select_related('candidate', 'program', 'testing_center', 'schedule_slot')
            .order_by('schedule_slot__date', 'schedule_slot__start_time')
        )
