"""SP2 views — programmes, applications, judging, cohorts, mentorship.

Each view either renders a full page (extends `layouts/dashboard.html`) or
returns an HTMX partial when the request originates from HTMX. Role gating
uses the mixins from `apps.smehub._shared.role_gates`.
"""
from __future__ import annotations

import json

from django.contrib import messages
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied, ValidationError
from django.db.models import Case, IntegerField, Q, Value, When
from django.http import FileResponse, Http404, HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.utils.dateparse import parse_date
from django.views import View
from django.views.decorators.http import require_GET, require_POST
from django.views.generic import (
    CreateView,
    DetailView,
    ListView,
    TemplateView,
    UpdateView,
)

from apps.core.lists import paginate_qs
from apps.core.permissions.roles import UserRole
from apps.smehub._shared.role_gates import (
    EntrepreneurRequired,
    JudgeRequired,
    MentorRequired,
    ProgrammeStaffRequired,
)
from apps.smehub.incubation import services
from apps.smehub.incubation.forms import (
    ApplicationDocumentForm,
    ApplicationForm,
    AssignJudgesForm,
    CancelSessionForm,
    CohortForm,
    CompleteSessionForm,
    JudgeScoreForm,
    ManualCompleteForm,
    ManualTrainingForm,
    MentorAssignmentForm,
    ProgrammeForm,
    ReadinessAssessmentForm,
    ReturnForInfoForm,
    RubricSectionFormSet,
    SessionForm,
    StartApplicationForm,
    WithdrawForm,
)
from apps.smehub.incubation.models import (
    Application,
    ApplicationDocument,
    Certificate,
    Cohort,
    CohortMember,
    JudgeAssignment,
    JudgeScore,
    MentorAssignment,
    MentorSession,
    Programme,
    ProgrammeCourseLink,
    ReadinessAssessment,
    ScoringRubric,
)
from apps.smehub.onboarding.models import Business, EntrepreneurProfile

User = get_user_model()


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _is_programme_staff(user) -> bool:
    if not user.is_authenticated:
        return False
    if user.is_superuser:
        return True
    return user.role in {
        UserRole.SYSTEM_ADMIN,
        UserRole.ADMIN,
        UserRole.SME_ADMIN,
        UserRole.PROGRAMME_OFFICER,
    }


def _judge_qs():
    return User.objects.filter(role=UserRole.JUDGE, is_active=True).order_by("first_name", "last_name", "email")


def _mentor_qs(*, member=None):
    """FRSME-INC026 — sector-aligned mentor selection.

    When ``member`` is supplied, mentors whose ``expertise_sectors`` overlap
    the member's business sector are surfaced first. Mentors without an
    expertise list are still included (sector-agnostic mentors), they just
    rank below sector-matched candidates. When no member is given, returns
    every active mentor in alphabetical order — preserves the legacy contract
    used by other views.
    """
    base = User.objects.filter(role=UserRole.MENTOR, is_active=True)
    if member is None:
        return base.order_by("first_name", "last_name", "email")
    sector = getattr(getattr(member, "business", None), "sector", None)
    if not sector:
        return base.order_by("first_name", "last_name", "email")
    # Mentors whose expertise_sectors JSON list contains the business sector
    # rank first; everyone else (including mentors with empty expertise) follows.
    # MentorProfile.user has related_name='+' (no reverse accessor) so we go
    # through MentorProfile.objects rather than User-side joins.
    from apps.smehub.onboarding.models import MentorProfile

    matched_ids = set(
        MentorProfile.objects.filter(
            user__role=UserRole.MENTOR,
            user__is_active=True,
            expertise_sectors__contains=[sector],
        ).values_list("user_id", flat=True),
    )
    # Must stay a QuerySet — callers assign this to a ModelChoiceField.queryset,
    # which calls .all() on it (a sorted() list here 500s the assign-mentor page).
    return base.annotate(
        _sector_rank=Case(
            When(pk__in=matched_ids, then=Value(0)),
            default=Value(1),
            output_field=IntegerField(),
        ),
    ).order_by("_sector_rank", "first_name", "last_name", "email")


# ---------------------------------------------------------------------------
# Hub landing — re-homes Browse / My programme / Mentor / Judge / Cohorts.
# ---------------------------------------------------------------------------

class IncubationHubView(LoginRequiredMixin, TemplateView):
    """Tabbed shell linking to existing Incubation routes by audience.

    Counts and recent activity come from existing services — no new business
    logic. The hub itself is a pure orchestration layer (Slice B.2).
    """

    template_name = "smehub/incubation/hub.html"

    def get_context_data(self, **kwargs):
        from apps.smehub.incubation.models import CohortMember, JudgeAssignment

        ctx = super().get_context_data(**kwargs)
        user = self.request.user
        is_staff = _is_programme_staff(user)
        is_judge = user.is_authenticated and getattr(user, "role", None) == UserRole.JUDGE
        is_mentor = user.is_authenticated and getattr(user, "role", None) == UserRole.MENTOR
        is_entrepreneur = user.is_authenticated and getattr(user, "role", None) == UserRole.ENTREPRENEUR
        ctx["is_staff"] = is_staff
        ctx["is_judge"] = is_judge
        ctx["is_mentor"] = is_mentor
        ctx["is_entrepreneur"] = is_entrepreneur

        # "Open" = published AND still inside the application window. Imported
        # historical calls are published (archive value) but past-deadline.
        open_qs = Programme.objects.filter(
            Q(status=Programme.Status.PUBLISHED)
            & (
                Q(application_deadline__isnull=True)
                | Q(application_deadline__gte=timezone.now())
            )
        )
        ctx["open_programmes"] = open_qs.order_by("-published_at")[:6]
        ctx["open_programmes_count"] = open_qs.count()

        if is_entrepreneur:
            ctx["my_applications"] = (
                Application.objects.filter(entrepreneur__user=user)
                .select_related("programme", "business")
                .order_by("-created_at")[:5]
            )
            ctx["my_cohort_memberships"] = (
                CohortMember.objects.filter(business__entrepreneur__user=user)
                .select_related("cohort__programme", "business")
                .order_by("-created_at")[:5]
            )

        if is_mentor:
            ctx["pending_judge_assignments"] = 0
            ctx["mentor_dashboard_url"] = "smehub_incubation:mentor_dashboard"

        if is_judge:
            # FRSME-INC018 — applications assigned to this judge that they have
            # not yet scored (JudgeAssignment has no score field; a judge is
            # "pending" until a JudgeScore row exists for the application).
            ctx["judge_queue_count"] = (
                JudgeAssignment.objects.filter(judge=user)
                .exclude(application__scores__judge=user)
                .distinct()
                .count()
            )

        if is_staff:
            ctx["staff_kpis"] = {
                "draft_programmes": Programme.objects.filter(
                    status=Programme.Status.DRAFT,
                ).count(),
                "published_programmes": Programme.objects.filter(
                    status=Programme.Status.PUBLISHED,
                ).count(),
                "open_applications": Application.objects.filter(
                    status__in=[
                        Application.Status.SUBMITTED,
                        Application.Status.UNDER_REVIEW,
                    ],
                ).count(),
            }
        return ctx


# ---------------------------------------------------------------------------
# Programme browsing  (public / authenticated)
# ---------------------------------------------------------------------------

class ProgrammeListView(LoginRequiredMixin, ListView):
    template_name = "smehub/incubation/programme_list.html"
    context_object_name = "programmes"
    paginate_by = 20

    def get_queryset(self):
        qs = Programme.objects.all().order_by("-published_at", "-created_at")
        status = self.request.GET.get("status") or "published"
        if status == "published":
            qs = qs.filter(status=Programme.Status.PUBLISHED)
        elif status == "draft":
            qs = qs.filter(status=Programme.Status.DRAFT)
        elif status == "closed":
            qs = qs.filter(status=Programme.Status.CLOSED)
        # entrepreneurs only see published/closed unless they're staff
        if not _is_programme_staff(self.request.user):
            qs = qs.filter(status__in=[Programme.Status.PUBLISHED, Programme.Status.CLOSED])
        sector = self.request.GET.get("sector")
        if sector:
            qs = qs.filter(target_sectors__icontains=sector)
        return qs


class ProgrammeDetailView(LoginRequiredMixin, DetailView):
    model = Programme
    slug_field = "slug"
    slug_url_kwarg = "slug"
    template_name = "smehub/incubation/programme_detail.html"
    context_object_name = "programme"

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        programme = ctx["programme"]
        ctx["rubric_sections"] = (
            programme.rubric.sections.all() if hasattr(programme, "rubric") else []
        )
        ctx["course_links"] = programme.course_links.all()
        # eligibility for the current user's verified businesses
        if self.request.user.is_authenticated and self.request.user.role == UserRole.ENTREPRENEUR:
            verified_businesses = Business.objects.filter(
                entrepreneur__user=self.request.user,
                verification_status=Business.Status.VERIFIED,
            )
            eligibility = []
            for biz in verified_businesses:
                ok, reason = services.check_eligibility(biz, programme)
                eligibility.append({"business": biz, "ok": ok, "reason": reason})
            ctx["eligibility"] = eligibility
        return ctx


# ---------------------------------------------------------------------------
# Programme management  (officer)
# ---------------------------------------------------------------------------

class ProgrammeManageListView(ProgrammeStaffRequired, ListView):
    template_name = "smehub/incubation/programme_manage_list.html"
    context_object_name = "programmes"
    paginate_by = 30

    def get_queryset(self):
        return Programme.objects.all().order_by("-created_at")


class ProgrammeCreateView(ProgrammeStaffRequired, CreateView):
    model = Programme
    form_class = ProgrammeForm
    template_name = "smehub/incubation/programme_form.html"

    def form_valid(self, form):
        programme = services.create_programme(officer=self.request.user, **form.cleaned_data)
        messages.success(self.request, f"Programme “{programme.title}” created. Add a rubric next.")
        return redirect("smehub_incubation:programme_rubric", pk=programme.pk)


class ProgrammeUpdateView(ProgrammeStaffRequired, UpdateView):
    model = Programme
    form_class = ProgrammeForm
    template_name = "smehub/incubation/programme_form.html"

    def get_success_url(self):
        return reverse("smehub_incubation:programme_detail", kwargs={"slug": self.object.slug})


class ProgrammeRubricView(ProgrammeStaffRequired, View):
    template_name = "smehub/incubation/programme_rubric.html"

    def _ctx(self, programme, formset):
        return {"programme": programme, "formset": formset, "rubric": programme.rubric}

    def get(self, request, pk):
        programme = get_object_or_404(Programme, pk=pk)
        rubric, _ = ScoringRubric.objects.get_or_create(programme=programme)
        formset = RubricSectionFormSet(instance=rubric)
        return render(request, self.template_name, self._ctx(programme, formset))

    def post(self, request, pk):
        programme = get_object_or_404(Programme, pk=pk)
        rubric, _ = ScoringRubric.objects.get_or_create(programme=programme)
        formset = RubricSectionFormSet(request.POST, instance=rubric)
        if not formset.is_valid():
            return render(request, self.template_name, self._ctx(programme, formset), status=400)
        formset.save()
        messages.success(request, "Rubric saved.")
        return redirect("smehub_incubation:programme_rubric", pk=programme.pk)


class ProgrammeCoursesView(ProgrammeStaffRequired, View):
    template_name = "smehub/incubation/programme_courses.html"

    def get(self, request, pk):
        programme = get_object_or_404(Programme, pk=pk)
        return render(
            request,
            self.template_name,
            {
                "programme": programme,
                "links": programme.course_links.all(),
            },
        )

    def post(self, request, pk):
        programme = get_object_or_404(Programme, pk=pk)
        moodle_course_id = (request.POST.get("moodle_course_id") or "").strip()
        if not moodle_course_id:
            return redirect("smehub_incubation:programme_courses", pk=programme.pk)
        action = request.POST.get("action") or "add"
        if action == "remove":
            programme.course_links.filter(moodle_course_id=moodle_course_id).delete()
            messages.success(request, f"Removed course {moodle_course_id}.")
        else:
            title = (request.POST.get("course_title") or "").strip()
            services.link_programme_course(
                programme,
                moodle_course_id=moodle_course_id,
                course_title=title,
                is_required=True,
            )
            messages.success(request, f"Linked Moodle course {title or moodle_course_id}.")
        return redirect("smehub_incubation:programme_courses", pk=programme.pk)


@require_POST
@login_required
def publish_programme_view(request, pk):
    if not _is_programme_staff(request.user):
        raise PermissionDenied
    programme = get_object_or_404(Programme, pk=pk)
    try:
        services.publish_programme(programme, by_user=request.user)
    except services.ProgrammeNotPublishable as exc:
        messages.error(request, str(exc))
        return redirect("smehub_incubation:programme_rubric", pk=programme.pk)
    messages.success(request, f"Programme “{programme.title}” published.")
    return redirect("smehub_incubation:programme_detail", slug=programme.slug)


@require_POST
@login_required
def close_programme_view(request, pk):
    if not _is_programme_staff(request.user):
        raise PermissionDenied
    programme = get_object_or_404(Programme, pk=pk)
    services.close_programme(programme, by_user=request.user)
    messages.success(request, f"Programme “{programme.title}” closed.")
    return redirect("smehub_incubation:programme_detail", slug=programme.slug)


@require_POST
@login_required
def notify_eligible_view(request, pk):
    if not _is_programme_staff(request.user):
        raise PermissionDenied
    programme = get_object_or_404(Programme, pk=pk)
    n = services.notify_eligible_entrepreneurs(programme)
    messages.success(request, f"Notified {n} eligible entrepreneur(s).")
    return redirect("smehub_incubation:programme_detail", slug=programme.slug)


# ---------------------------------------------------------------------------
# Application — entrepreneur side
# ---------------------------------------------------------------------------

@login_required
def start_application_view(request, slug):
    if request.user.role != UserRole.ENTREPRENEUR:
        raise PermissionDenied
    programme = get_object_or_404(Programme, slug=slug)
    # FRSME-INC014 — don't render the business-pick form for a closed call;
    # the POST path already rejects via check_eligibility, mirror it on GET.
    if programme.application_deadline and programme.application_deadline < timezone.now():
        messages.error(
            request,
            f"Applications closed — the deadline for “{programme.title}” passed on "
            f"{programme.application_deadline:%d %b %Y}.",
        )
        return redirect("smehub_incubation:programme_detail", slug=programme.slug)
    business_qs = Business.objects.filter(
        entrepreneur__user=request.user,
        verification_status=Business.Status.VERIFIED,
    )
    # FRSME-MEI002 — applications need an SP1 baseline; tell the entrepreneur
    # up-front instead of letting the POST blow up in the service layer.
    profile = business_qs.first().entrepreneur if business_qs.exists() else None
    if profile is not None and not services.entrepreneur_has_baseline(profile):
        messages.error(
            request,
            "Your SME-Hub baseline has not been initialised yet. Complete your "
            "business baseline and innovation-hub affiliation in onboarding "
            "before applying to a programme.",
        )
        return redirect("smehub_incubation:programme_detail", slug=programme.slug)
    if request.method == "POST":
        form = StartApplicationForm(request.POST, business_qs=business_qs)
        if form.is_valid():
            business = form.cleaned_data["business"]
            try:
                application = services.start_application(
                    entrepreneur=business.entrepreneur,
                    business=business,
                    programme=programme,
                )
            except (services.EligibilityError, services.IncubationStateError) as exc:
                messages.error(request, str(exc))
                return redirect("smehub_incubation:programme_detail", slug=programme.slug)
            return redirect("smehub_incubation:application_wizard", pk=application.pk)
    else:
        form = StartApplicationForm(business_qs=business_qs)
    return render(
        request,
        "smehub/incubation/application_start.html",
        {"programme": programme, "form": form, "verified_businesses": business_qs},
    )


class ApplicationWizardView(EntrepreneurRequired, View):
    template_name = "smehub/incubation/application_wizard.html"

    def _get_app(self, request, pk):
        app = get_object_or_404(
            Application.objects.select_related("programme", "business"),
            pk=pk,
            entrepreneur__user=request.user,
        )
        return app

    def _ctx(self, app, form):
        documents = list(app.documents.all())
        uploaded_labels = {(d.label or "").strip().lower() for d in documents}
        required = [
            str(d).strip() for d in (app.programme.required_document_labels or []) if str(d).strip()
        ]
        required_docs = [
            {"label": label, "satisfied": label.lower() in uploaded_labels}
            for label in required
        ]
        missing_required = [d["label"] for d in required_docs if not d["satisfied"]]
        return {
            "application": app,
            "form": form,
            "documents": documents,
            "doc_form": ApplicationDocumentForm(),
            "required_docs": required_docs,
            "missing_required": missing_required,
            "docs_editable": app.status in {
                Application.Status.DRAFT,
                Application.Status.RETURNED_FOR_INFO,
            },
        }

    def get(self, request, pk):
        app = self._get_app(request, pk)
        form = ApplicationForm(instance=app)
        return render(request, self.template_name, self._ctx(app, form))

    def post(self, request, pk):
        app = self._get_app(request, pk)
        form = ApplicationForm(request.POST, instance=app)
        if not form.is_valid():
            return render(request, self.template_name, self._ctx(app, form), status=400)
        services.auto_save_application(app, payload=form.cleaned_data)
        messages.success(request, "Saved.")
        return redirect("smehub_incubation:application_wizard", pk=app.pk)


@require_POST
@login_required
def application_document_upload_view(request, pk):
    """FRSME-INC010 — attach a supporting document to an application."""
    if getattr(request.user, "role", None) != UserRole.ENTREPRENEUR:
        raise PermissionDenied("Only entrepreneurs may upload application documents.")
    app = get_object_or_404(Application, pk=pk, entrepreneur__user=request.user)
    if app.status not in {Application.Status.DRAFT, Application.Status.RETURNED_FOR_INFO}:
        messages.error(request, "Documents can only be attached while the application is a draft.")
        return redirect("smehub_incubation:application_wizard", pk=app.pk)
    form = ApplicationDocumentForm(request.POST, request.FILES)
    if not form.is_valid():
        messages.error(request, "Give the document a label and choose a file to upload.")
        return redirect("smehub_incubation:application_wizard", pk=app.pk)
    document = form.save(commit=False)
    document.application = app
    document.save()
    messages.success(request, f"Attached “{document.label}”.")
    return redirect("smehub_incubation:application_wizard", pk=app.pk)


@require_POST
@login_required
def application_document_delete_view(request, pk, doc_pk):
    """FRSME-INC010 — remove a supporting document from an application."""
    if getattr(request.user, "role", None) != UserRole.ENTREPRENEUR:
        raise PermissionDenied("Only entrepreneurs may remove application documents.")
    app = get_object_or_404(Application, pk=pk, entrepreneur__user=request.user)
    document = get_object_or_404(ApplicationDocument, pk=doc_pk, application=app)
    if app.status not in {Application.Status.DRAFT, Application.Status.RETURNED_FOR_INFO}:
        messages.error(request, "Documents can only be changed while the application is a draft.")
        return redirect("smehub_incubation:application_wizard", pk=app.pk)
    label = document.label
    document.delete()
    messages.success(request, f"Removed “{label}”.")
    return redirect("smehub_incubation:application_wizard", pk=app.pk)


@require_POST
@login_required
def application_auto_save_view(request, pk):
    """HTMX endpoint — silent save, returns 204.

    Audit fix: the FK filter (``entrepreneur__user=request.user``) already
    guarantees ownership but the legacy view lacked an explicit role check.
    Reject requests from non-entrepreneur roles up front so the gate is
    self-documenting and consistent with the wider SP1 pattern.
    """
    if request.user.role != UserRole.ENTREPRENEUR:
        raise PermissionDenied("Only entrepreneurs may auto-save applications.")
    app = get_object_or_404(Application, pk=pk, entrepreneur__user=request.user)
    payload = {
        "business_description": request.POST.get("business_description", ""),
        "problem_statement": request.POST.get("problem_statement", ""),
        "traction": request.POST.get("traction", ""),
        "funding_needs": request.POST.get("funding_needs", ""),
        "support_sought": request.POST.get("support_sought", ""),
    }
    try:
        services.auto_save_application(app, payload=payload)
    except services.ApplicationStateError:
        return HttpResponse(status=409)
    return HttpResponse(status=204)


@require_POST
@login_required
def application_submit_view(request, pk):
    # FRSME-INC explicit role gate — FK filter alone allowed any logged-in
    # user with an EntrepreneurProfile to bypass the role check.
    if getattr(request.user, "role", None) != UserRole.ENTREPRENEUR:
        raise PermissionDenied("Only entrepreneurs may submit applications.")
    app = get_object_or_404(Application, pk=pk, entrepreneur__user=request.user)
    try:
        services.submit_application(app)
    except services.ApplicationStateError as exc:
        messages.error(request, str(exc))
        return redirect("smehub_incubation:application_wizard", pk=app.pk)
    messages.success(request, f"Application {app.application_id} submitted.")
    return redirect("smehub_incubation:my_programme")


@require_POST
@login_required
def application_withdraw_view(request, pk):
    if getattr(request.user, "role", None) != UserRole.ENTREPRENEUR:
        raise PermissionDenied("Only entrepreneurs may withdraw applications.")
    app = get_object_or_404(Application, pk=pk, entrepreneur__user=request.user)
    try:
        app.withdraw()
        app.save()
    except Exception as exc:  # noqa: BLE001 - FSM transition errors
        messages.error(request, f"Cannot withdraw: {exc}")
        return redirect("smehub_incubation:application_wizard", pk=app.pk)
    messages.success(request, "Application withdrawn.")
    return redirect("smehub_incubation:my_programme")


# ---------------------------------------------------------------------------
# Application review — officer side
# ---------------------------------------------------------------------------

class ApplicationReviewQueueView(ProgrammeStaffRequired, ListView):
    """FRSME-INC015 — first-level officer triage list (Slice B.3a).

    Re-uses ``Application`` queryset and renders a paginated list filterable
    by status, sector, and programme. Each row links to the existing
    ``ApplicationDetailView`` so no admin-side logic is duplicated.
    """

    template_name = "smehub/incubation/application_review_queue.html"
    context_object_name = "applications"
    paginate_by = 25

    def get_queryset(self):
        statuses = [Application.Status.SUBMITTED, Application.Status.UNDER_REVIEW]
        status_filter = self.request.GET.get("status")
        if status_filter == "submitted":
            statuses = [Application.Status.SUBMITTED]
        elif status_filter == "under_review":
            statuses = [Application.Status.UNDER_REVIEW]
        qs = (
            Application.objects.filter(status__in=statuses)
            .select_related("entrepreneur__user", "programme", "business")
            .order_by("-submitted_at", "-created_at")
        )
        programme_id = self.request.GET.get("programme")
        if programme_id and programme_id.isdigit():
            qs = qs.filter(programme_id=int(programme_id))
        sector = self.request.GET.get("sector")
        if sector:
            qs = qs.filter(business__sector__icontains=sector)
        # FRSME-INC015 — filter by submission date range (inclusive). The "to"
        # bound is treated as end-of-day so a same-day from/to pair still matches.
        submitted_from = parse_date(self.request.GET.get("submitted_from") or "")
        if submitted_from:
            qs = qs.filter(submitted_at__date__gte=submitted_from)
        submitted_to = parse_date(self.request.GET.get("submitted_to") or "")
        if submitted_to:
            qs = qs.filter(submitted_at__date__lte=submitted_to)
        return qs

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["status_filter"] = self.request.GET.get("status", "all")
        ctx["programme_filter"] = self.request.GET.get("programme", "")
        ctx["sector_filter"] = self.request.GET.get("sector", "")
        ctx["submitted_from_filter"] = self.request.GET.get("submitted_from", "")
        ctx["submitted_to_filter"] = self.request.GET.get("submitted_to", "")
        ctx["programmes"] = Programme.objects.filter(
            status__in=[Programme.Status.PUBLISHED, Programme.Status.CLOSED],
        ).order_by("title")
        ctx["totals"] = {
            "submitted": Application.objects.filter(status=Application.Status.SUBMITTED).count(),
            "under_review": Application.objects.filter(status=Application.Status.UNDER_REVIEW).count(),
        }
        return ctx


class ApplicationDetailView(ProgrammeStaffRequired, DetailView):
    model = Application
    template_name = "smehub/incubation/application_detail.html"
    context_object_name = "application"

    def get_queryset(self):
        # Slice C — kill the N+1 chain that hit on every officer page load.
        return Application.objects.select_related(
            "entrepreneur__user",
            "programme",
            "business",
        ).prefetch_related("documents")

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        app = ctx["application"]
        ctx["score"] = services.compute_weighted_average(app)
        ctx["judges"] = JudgeAssignment.objects.filter(application=app).select_related("judge")
        ctx["scores"] = JudgeScore.objects.filter(application=app).select_related("judge", "section")
        # FRSME-INC015/16 — entrepreneur-uploaded supporting documents must be
        # visible to the reviewing officer, not just inside the wizard.
        ctx["documents"] = app.documents.all()
        ctx["return_form"] = ReturnForInfoForm()
        return ctx


@require_POST
@login_required
def application_begin_review_view(request, pk):
    if not _is_programme_staff(request.user):
        raise PermissionDenied
    app = get_object_or_404(Application, pk=pk)
    try:
        services.pass_first_level_review(app, officer=request.user)
    except services.ApplicationStateError as exc:
        messages.error(request, str(exc))
    else:
        messages.success(request, "Marked under review.")
    return redirect("smehub_incubation:application_detail", pk=app.pk)


@require_POST
@login_required
def application_return_view(request, pk):
    if not _is_programme_staff(request.user):
        raise PermissionDenied
    app = get_object_or_404(Application, pk=pk)
    form = ReturnForInfoForm(request.POST)
    if not form.is_valid():
        messages.error(request, "Message is required.")
    else:
        services.return_for_info(app, officer=request.user, message=form.cleaned_data["message"])
        messages.success(request, "Returned to applicant for more info.")
    return redirect("smehub_incubation:application_detail", pk=app.pk)


class AssignJudgesView(ProgrammeStaffRequired, View):
    template_name = "smehub/incubation/application_assign_judges.html"

    def get(self, request, pk):
        app = get_object_or_404(Application, pk=pk)
        form = AssignJudgesForm(judge_qs=_judge_qs())
        return render(request, self.template_name, {"application": app, "form": form})

    def post(self, request, pk):
        app = get_object_or_404(Application, pk=pk)
        form = AssignJudgesForm(request.POST, judge_qs=_judge_qs())
        if not form.is_valid():
            return render(request, self.template_name, {"application": app, "form": form}, status=400)
        try:
            services.assign_judges(app, judges=list(form.cleaned_data["judges"]), by_user=request.user)
        except services.ApplicationStateError as exc:
            messages.error(request, str(exc))
        else:
            messages.success(request, "Judges assigned.")
        return redirect("smehub_incubation:application_detail", pk=app.pk)


# ---------------------------------------------------------------------------
# Judge dashboard + scoring
# ---------------------------------------------------------------------------

class JudgeQueueView(JudgeRequired, ListView):
    template_name = "smehub/incubation/judge_queue.html"
    context_object_name = "assignments"
    paginate_by = 30

    def get_queryset(self):
        return (
            JudgeAssignment.objects.filter(judge=self.request.user)
            .select_related("application__programme", "application__business")
            .order_by("-created_at")
        )

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        scored_app_ids = set(
            JudgeScore.objects.filter(judge=self.request.user).values_list("application_id", flat=True),
        )
        ctx["scored_app_ids"] = scored_app_ids
        return ctx


class JudgeScoreView(JudgeRequired, View):
    template_name = "smehub/incubation/judge_score_form.html"

    def _ctx(self, application, form):
        # FRSME-INC018 — judges score against the full submission, including
        # the entrepreneur's supporting documents.
        return {
            "application": application,
            "form": form,
            "documents": application.documents.all(),
        }

    def get(self, request, pk):
        application = get_object_or_404(Application.objects.select_related("programme", "business"), pk=pk)
        if not JudgeAssignment.objects.filter(application=application, judge=request.user).exists():
            raise PermissionDenied
        sections = list(application.programme.rubric.sections.all())
        existing = {
            js.section_id: js.score
            for js in JudgeScore.objects.filter(application=application, judge=request.user)
        }
        form = JudgeScoreForm(sections=sections, initial_scores=existing)
        return render(request, self.template_name, self._ctx(application, form))

    def post(self, request, pk):
        application = get_object_or_404(Application.objects.select_related("programme"), pk=pk)
        if not JudgeAssignment.objects.filter(application=application, judge=request.user).exists():
            raise PermissionDenied
        sections = list(application.programme.rubric.sections.all())
        form = JudgeScoreForm(request.POST, sections=sections)
        if not form.is_valid():
            return render(request, self.template_name, self._ctx(application, form), status=400)
        try:
            services.submit_score(
                application,
                judge=request.user,
                section_scores=form.section_scores(),
                comments=form.cleaned_data.get("comments", ""),
            )
        except (services.ApplicationStateError, ValidationError) as exc:
            messages.error(request, str(exc))
            return render(request, self.template_name, self._ctx(application, form), status=400)
        messages.success(request, "Scores submitted.")
        return redirect("smehub_incubation:judge_queue")


# ---------------------------------------------------------------------------
# Selection dashboard + cohort confirmation
# ---------------------------------------------------------------------------

class SelectionDashboardView(ProgrammeStaffRequired, View):
    template_name = "smehub/incubation/selection_dashboard.html"

    def get(self, request, pk):
        programme = get_object_or_404(Programme, pk=pk)
        ranked = services.rank_applications(programme)
        # NOTE: intentionally NOT paginated — accept checkboxes across the full
        # ranked list POST together (confirm_cohort_view reads getlist("accept")),
        # so off-page rows must stay in the DOM.
        return render(
            request,
            self.template_name,
            {"programme": programme, "ranked": ranked, "cohort_form": CohortForm()},
        )


@require_POST
@login_required
def confirm_cohort_view(request, pk):
    if not _is_programme_staff(request.user):
        raise PermissionDenied
    programme = get_object_or_404(Programme, pk=pk)
    form = CohortForm(request.POST)
    if not form.is_valid():
        messages.error(request, "Cohort details required.")
        return redirect("smehub_incubation:selection_dashboard", pk=programme.pk)
    accepted_ids_raw = request.POST.getlist("accept")
    accepted_ids = [int(x) for x in accepted_ids_raw if x.isdigit()]
    cohort = services.confirm_cohort(
        programme,
        accepted_application_ids=accepted_ids,
        name=form.cleaned_data["name"],
        start_date=form.cleaned_data["start_date"],
        end_date=form.cleaned_data["end_date"],
        venue=form.cleaned_data.get("venue", ""),
        rejection_reason=(request.POST.get("rejection_reason") or "").strip(),
        by_user=request.user,
    )
    messages.success(request, f"Cohort “{cohort.name}” confirmed with {cohort.members.count()} member(s).")
    return redirect("smehub_incubation:cohort_detail", pk=cohort.pk)


# ---------------------------------------------------------------------------
# Cohorts
# ---------------------------------------------------------------------------

class CohortListView(ProgrammeStaffRequired, ListView):
    template_name = "smehub/incubation/cohort_list.html"
    context_object_name = "cohorts"
    paginate_by = 30

    def get_queryset(self):
        return Cohort.objects.select_related("programme").order_by("-start_date")


class CohortDetailView(ProgrammeStaffRequired, DetailView):
    model = Cohort
    template_name = "smehub/incubation/cohort_detail.html"
    context_object_name = "cohort"

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        members_qs = (
            self.object.members.select_related("entrepreneur__user", "business", "progress")
            .prefetch_related("mentor_assignments__mentor")
        )
        # Totals computed over the full roster so pagination can't skew the stat
        # strip (the page only holds one slice of members).
        ctx["member_count"] = members_qs.count()
        ctx["mentor_active_count"] = (
            members_qs.filter(mentor_assignments__ended_at__isnull=True).distinct().count()
        )
        ctx["members"] = paginate_qs(self.request, members_qs, per_page=30, param="member_page")
        ctx["course_links"] = self.object.programme.course_links.all()
        # FRSME-INC033 — per-member completion-criteria standing for the roster
        # slice on this page, so officers see who is outstanding on what.
        for m in ctx["members"]:
            m.completion_breakdown = (
                services.completion_criteria_breakdown(m)
                if m.status == CohortMember.Status.ACTIVE
                else None
            )
        programme = self.object.programme
        ctx["completion_config"] = {
            "min_mentorship_sessions": programme.min_mentorship_sessions,
            "attendance_threshold_pct": programme.attendance_threshold_pct,
            "required_courses": programme.course_links.filter(is_required=True).count(),
        }
        return ctx


@require_POST
@login_required
def enrol_cohort_in_courses_view(request, pk):
    if not _is_programme_staff(request.user):
        raise PermissionDenied
    cohort = get_object_or_404(Cohort, pk=pk)
    summary = services.enrol_cohort_in_courses(cohort, by_user=request.user)
    enrolled = sum(s["enrolled"] for s in summary.values())
    failed = sum(s["failed"] for s in summary.values())
    messages.success(request, f"Enrolled {enrolled} member-course slot(s); {failed} failed (already enrolled or ineligible).")
    return redirect("smehub_incubation:cohort_detail", pk=cohort.pk)


@require_POST
@login_required
def cohort_member_retry_enrolment_view(request, pk):
    """FRSME-INC025 — admin / programme officer retries a single member's REP enrolment."""
    if not _is_programme_staff(request.user):
        raise PermissionDenied
    member = get_object_or_404(CohortMember, pk=pk)
    result = services.retry_cohort_enrolment(member, by_user=request.user)
    if result["errors"]:
        messages.warning(
            request,
            f"Retry for {member.business.name}: {result['enrolled']} succeeded, "
            f"{result['failed']} failed. See member row for details.",
        )
    else:
        messages.success(
            request,
            f"Retry for {member.business.name}: enrolled into "
            f"{result['enrolled']} course(s).",
        )
    return redirect("smehub_incubation:cohort_detail", pk=member.cohort.pk)


class AssignMentorView(ProgrammeStaffRequired, View):
    template_name = "smehub/incubation/assign_mentor.html"

    def get(self, request, pk):
        member = get_object_or_404(CohortMember, pk=pk)
        form = MentorAssignmentForm(mentor_qs=_mentor_qs(member=member))
        return render(request, self.template_name, {"member": member, "form": form})

    def post(self, request, pk):
        member = get_object_or_404(CohortMember, pk=pk)
        form = MentorAssignmentForm(request.POST, mentor_qs=_mentor_qs(member=member))
        if not form.is_valid():
            return render(request, self.template_name, {"member": member, "form": form}, status=400)
        services.assign_mentor(
            member,
            mentor=form.cleaned_data["mentor"],
            by_user=request.user,
            notes=form.cleaned_data.get("notes", ""),
        )
        messages.success(request, "Mentor assigned.")
        return redirect("smehub_incubation:cohort_detail", pk=member.cohort.pk)


class WithdrawMemberView(ProgrammeStaffRequired, View):
    template_name = "smehub/incubation/cohort_withdraw.html"

    def get(self, request, pk):
        member = get_object_or_404(CohortMember, pk=pk)
        return render(request, self.template_name, {"member": member, "form": WithdrawForm()})

    def post(self, request, pk):
        member = get_object_or_404(CohortMember, pk=pk)
        form = WithdrawForm(request.POST)
        if not form.is_valid():
            return render(request, self.template_name, {"member": member, "form": form}, status=400)
        services.withdraw_cohort_member(member, by_user=request.user, reason=form.cleaned_data["reason"])
        messages.success(request, "Member withdrawn — data preserved.")
        return redirect("smehub_incubation:cohort_detail", pk=member.cohort.pk)


class ManualCompleteView(ProgrammeStaffRequired, View):
    template_name = "smehub/incubation/cohort_manual_complete.html"

    def get(self, request, pk):
        member = get_object_or_404(CohortMember, pk=pk)
        return render(request, self.template_name, {"member": member, "form": ManualCompleteForm()})

    def post(self, request, pk):
        member = get_object_or_404(CohortMember, pk=pk)
        form = ManualCompleteForm(request.POST)
        if not form.is_valid():
            return render(request, self.template_name, {"member": member, "form": form}, status=400)
        try:
            services.manual_complete(member, by_user=request.user, notes=form.cleaned_data.get("notes", ""))
        except services.ApplicationStateError as exc:
            messages.error(request, str(exc))
            return redirect("smehub_incubation:cohort_detail", pk=member.cohort.pk)
        messages.success(request, "Member completed; certificate generated.")
        return redirect("smehub_incubation:cohort_detail", pk=member.cohort.pk)


class RecordManualTrainingView(ProgrammeStaffRequired, View):
    template_name = "smehub/incubation/cohort_manual_training.html"

    def get(self, request, pk):
        member = get_object_or_404(CohortMember, pk=pk)
        return render(request, self.template_name, {"member": member, "form": ManualTrainingForm()})

    def post(self, request, pk):
        member = get_object_or_404(CohortMember, pk=pk)
        form = ManualTrainingForm(request.POST)
        if not form.is_valid():
            return render(request, self.template_name, {"member": member, "form": form}, status=400)
        services.record_manual_training_completion(
            member,
            title=form.cleaned_data["title"],
            completed_at=form.cleaned_data.get("completed_at"),
            by_user=request.user,
        )
        messages.success(request, "Training recorded.")
        return redirect("smehub_incubation:cohort_detail", pk=member.cohort.pk)


# ---------------------------------------------------------------------------
# Mentor dashboard + sessions
# ---------------------------------------------------------------------------

class MentorDashboardView(MentorRequired, TemplateView):
    template_name = "smehub/incubation/mentor_dashboard.html"

    def get_context_data(self, **kwargs):
        from datetime import timedelta

        ctx = super().get_context_data(**kwargs)
        user = self.request.user
        now = timezone.now()
        quarter_start = now - timedelta(days=90)
        stale_threshold = now - timedelta(days=30)

        active_assignments = list(
            MentorAssignment.objects.filter(mentor=user, ended_at__isnull=True)
            .select_related("cohort_member__cohort__programme", "cohort_member__business", "cohort_member__entrepreneur__user")
            .prefetch_related("cohort_member__readiness_assessments")
        )

        # Decorate each assignment with the latest assessment + gaps.
        mentees_needing_assessment = 0
        for a in active_assignments:
            latest = (
                a.cohort_member.readiness_assessments.order_by("-conducted_at").first()
            )
            a.latest_assessment = latest
            if latest is None or latest.conducted_at < stale_threshold:
                mentees_needing_assessment += 1

        ctx["assignments"] = active_assignments

        sessions = MentorSession.objects.filter(assignment__mentor=user)
        completed_sessions = sessions.filter(status=MentorSession.Status.COMPLETED)
        # Aggregate completed minutes → hours for the stat tile.
        total_completed_minutes = sum(
            s.duration_minutes or 0 for s in completed_sessions
        )

        ctx["stats"] = {
            "active_mentees": len(active_assignments),
            "completed_hours": round(total_completed_minutes / 60, 1) if total_completed_minutes else 0,
            "needs_assessment": mentees_needing_assessment,
            "upcoming": sessions.filter(
                status=MentorSession.Status.SCHEDULED,
                starts_at__gte=now,
            ).count(),
            "this_quarter": completed_sessions.filter(completed_at__gte=quarter_start).count(),
        }

        ctx["upcoming_sessions"] = (
            sessions.filter(status=MentorSession.Status.SCHEDULED, starts_at__gte=now)
            .select_related("assignment__cohort_member__business")
            .order_by("starts_at")[:20]
        )
        ctx["recent_sessions"] = (
            completed_sessions
            .select_related("assignment__cohort_member__business")
            .order_by("-completed_at")[:10]
        )
        ctx["cancelled_sessions"] = (
            sessions.filter(status=MentorSession.Status.CANCELLED)
            .select_related("assignment__cohort_member__business")
            .order_by("-updated_at")[:10]
        )

        # Direct meeting requests (mentor directory → MeetingRequest inbox).
        from django.contrib.contenttypes.models import ContentType

        from apps.smehub.onboarding.models import MeetingRequest, MentorProfile

        mentor_profile = MentorProfile.objects.filter(user=user).first()
        ctx["pending_meeting_requests"] = (
            MeetingRequest.objects.filter(
                recipient_content_type=ContentType.objects.get_for_model(MentorProfile),
                recipient_object_id=mentor_profile.pk,
                status=MeetingRequest.Status.REQUEST,
            ).count()
            if mentor_profile
            else 0
        )
        return ctx


class BookSessionView(MentorRequired, View):
    template_name = "smehub/incubation/session_form.html"

    def get(self, request, pk):
        assignment = get_object_or_404(MentorAssignment, pk=pk, mentor=request.user)
        return render(request, self.template_name, {"assignment": assignment, "form": SessionForm()})

    def post(self, request, pk):
        assignment = get_object_or_404(MentorAssignment, pk=pk, mentor=request.user)
        form = SessionForm(request.POST)
        if not form.is_valid():
            return render(request, self.template_name, {"assignment": assignment, "form": form}, status=400)
        services.book_session(
            assignment,
            starts_at=form.cleaned_data["starts_at"],
            duration_minutes=form.cleaned_data.get("duration_minutes") or 60,
            type=form.cleaned_data.get("type") or MentorSession.Type.VIRTUAL,
            location=form.cleaned_data.get("location", ""),
            agenda=form.cleaned_data.get("agenda", ""),
            booked_by=request.user,
        )
        messages.success(request, "Session scheduled.")
        return redirect("smehub_incubation:mentor_dashboard")


@require_POST
@login_required
def complete_session_view(request, pk):
    session = get_object_or_404(MentorSession, pk=pk)
    if request.user != session.assignment.mentor and not _is_programme_staff(request.user):
        raise PermissionDenied
    form = CompleteSessionForm(request.POST)
    notes = form.cleaned_data.get("notes", "") if form.is_valid() else ""
    services.complete_session(session, notes=notes, by_user=request.user)
    messages.success(request, "Session marked complete.")
    return redirect("smehub_incubation:mentor_dashboard")


@require_POST
@login_required
def cancel_session_view(request, pk):
    session = get_object_or_404(MentorSession, pk=pk)
    if request.user != session.assignment.mentor and not _is_programme_staff(request.user):
        raise PermissionDenied
    form = CancelSessionForm(request.POST)
    reason = form.cleaned_data.get("reason", "") if form.is_valid() else ""
    services.cancel_session(session, reason=reason)
    messages.success(request, "Session cancelled.")
    return redirect("smehub_incubation:mentor_dashboard")


# ---------------------------------------------------------------------------
# Mentorship Readiness Assessment  (PRD §5.X — Mentorship and Industry Advisory)
# ---------------------------------------------------------------------------

def _user_can_view_readiness(user, member: CohortMember) -> bool:
    """Entrepreneur owner, currently-assigned mentor, or programme staff."""
    if member.entrepreneur.user_id == user.pk:
        return True
    if _is_programme_staff(user):
        return True
    return member.mentor_assignments.filter(
        mentor=user, ended_at__isnull=True,
    ).exists()


def _user_can_record_readiness(user, member: CohortMember) -> bool:
    """Programme staff or the active mentor on this assignment."""
    if _is_programme_staff(user):
        return True
    return member.mentor_assignments.filter(
        mentor=user, ended_at__isnull=True,
    ).exists()


class ReadinessAssessmentListView(LoginRequiredMixin, TemplateView):
    template_name = "smehub/incubation/readiness_list.html"

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        member = get_object_or_404(
            CohortMember.objects.select_related(
                "cohort__programme", "business", "entrepreneur__user",
            ),
            pk=kwargs["pk"],
        )
        if not _user_can_view_readiness(self.request.user, member):
            raise PermissionDenied
        ctx["member"] = member
        ctx["assessments"] = paginate_qs(
            self.request,
            ReadinessAssessment.objects.filter(cohort_member=member)
            .select_related("conducted_by"),
            param="assess_page",
        )
        ctx["can_record"] = _user_can_record_readiness(self.request.user, member)
        return ctx


class RecordReadinessAssessmentView(LoginRequiredMixin, View):
    template_name = "smehub/incubation/readiness_form.html"

    def _get_member(self, pk: int) -> CohortMember:
        member = get_object_or_404(
            CohortMember.objects.select_related(
                "cohort__programme", "business", "entrepreneur__user",
            ),
            pk=pk,
        )
        if not _user_can_record_readiness(self.request.user, member):
            raise PermissionDenied
        return member

    def get(self, request, pk):
        member = self._get_member(pk)
        return render(request, self.template_name, {
            "member": member,
            "form": ReadinessAssessmentForm(),
        })

    def post(self, request, pk):
        member = self._get_member(pk)
        form = ReadinessAssessmentForm(request.POST)
        if not form.is_valid():
            return render(request, self.template_name, {
                "member": member,
                "form": form,
            }, status=400)
        try:
            services.record_readiness_assessment(
                member,
                market_positioning_score=form.cleaned_data["market_positioning_score"],
                technical_robustness_score=form.cleaned_data["technical_robustness_score"],
                regulatory_compliance_score=form.cleaned_data["regulatory_compliance_score"],
                market_positioning_notes=form.cleaned_data.get("market_positioning_notes", ""),
                technical_robustness_notes=form.cleaned_data.get("technical_robustness_notes", ""),
                regulatory_compliance_notes=form.cleaned_data.get("regulatory_compliance_notes", ""),
                overall_notes=form.cleaned_data.get("overall_notes", ""),
                by_user=request.user,
            )
        except services.IncubationStateError as exc:
            messages.error(request, str(exc))
            return render(request, self.template_name, {
                "member": member,
                "form": form,
            }, status=400)
        messages.success(request, "Readiness assessment recorded.")
        return redirect("smehub_incubation:readiness_list", pk=member.pk)


# ---------------------------------------------------------------------------
# Entrepreneur "My programme" dashboard
# ---------------------------------------------------------------------------

class MyProgrammeView(EntrepreneurRequired, TemplateView):
    template_name = "smehub/incubation/my_programme.html"

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        user = self.request.user
        ctx["applications"] = paginate_qs(
            self.request,
            Application.objects.filter(entrepreneur__user=user)
            .select_related("programme", "business")
            .order_by("-created_at"),
            per_page=10,
            param="app_page",
        )
        ctx["memberships"] = paginate_qs(
            self.request,
            CohortMember.objects.filter(entrepreneur__user=user)
            .select_related("cohort__programme", "business", "progress", "certificate")
            .order_by("-created_at"),
            per_page=10,
            param="member_page",
        )
        ctx["upcoming_sessions"] = (
            MentorSession.objects.filter(
                assignment__cohort_member__entrepreneur__user=user,
                status=MentorSession.Status.SCHEDULED,
                starts_at__gte=timezone.now(),
            )
            .select_related("assignment__mentor", "assignment__cohort_member__business")
            .order_by("starts_at")[:10]
        )
        # FRSME-INC034 — real issued-certificate count + which memberships have a
        # downloadable PDF, so the stat card and the "Download certificate" link
        # read from the DB instead of a (broken) template-side accumulator.
        member_ids = list(
            CohortMember.objects.filter(entrepreneur__user=user).values_list("pk", flat=True)
        )
        certs = Certificate.objects.filter(cohort_member_id__in=member_ids)
        ctx["certificates_count"] = certs.count()
        ctx["cert_member_ids"] = set(
            certs.exclude(pdf_file="").values_list("cohort_member_id", flat=True)
        )
        # FRSME-INC033 — completion-criteria breakdown per membership on this page,
        # so the entrepreneur sees exactly what is outstanding before completion.
        for m in ctx["memberships"]:
            m.completion_breakdown = (
                services.completion_criteria_breakdown(m)
                if m.status == CohortMember.Status.ACTIVE
                else None
            )
        return ctx


@login_required
def certificate_download_view(request, pk):
    """FRSME-INC034 — stream a completed member's certificate PDF.

    Owner-gated: the entrepreneur who owns the cohort membership, or programme
    staff, may download. Streamed off disk with ``FileResponse`` so large PDFs
    do not balloon memory.
    """
    cert = get_object_or_404(
        Certificate.objects.select_related(
            "cohort_member__entrepreneur__user",
            "cohort_member__cohort__programme",
        ),
        pk=pk,
    )
    member = cert.cohort_member
    if member.entrepreneur.user_id != request.user.pk and not _is_programme_staff(request.user):
        raise PermissionDenied
    if not cert.pdf_file:
        raise Http404("Certificate PDF is not available yet.")
    return FileResponse(
        cert.pdf_file.open("rb"),
        content_type="application/pdf",
        as_attachment=True,
        filename=f"certificate-{cert.certificate_number}.pdf",
    )


# ---------------------------------------------------------------------------
# Competitions — calls with a dynamic application form + judged scores
# ---------------------------------------------------------------------------


class CompetitionListView(ProgrammeStaffRequired, ListView):
    """Competitions and external calls (dynamic application form + scores)."""

    template_name = "smehub/incubation/competition_list.html"
    context_object_name = "competitions"
    paginate_by = 20

    def get_queryset(self):
        return (
            Programme.objects.exclude(call_type=Programme.CallType.PROGRAMME)
            .order_by("-published_at", "-created_at")
        )

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["competition_count"] = self.get_queryset().count()
        return ctx


class CompetitionDetailView(ProgrammeStaffRequired, View):
    """Render a competition: its dynamic form, applications (with
    label-resolved answers) and judged scores."""

    template_name = "smehub/incubation/competition_detail.html"

    def get(self, request, pk):
        programme = get_object_or_404(Programme, pk=pk)
        # Form questions (label + type) from the captured formBuilder schema.
        form_fields = [
            {
                "name": f.get("name", ""),
                "label": f.get("label") or f.get("name", ""),
                "type": f.get("type", ""),
                "is_header": f.get("type") == "header",
            }
            for f in (programme.application_form_schema or [])
            if isinstance(f, dict)
        ]
        applications = (
            programme.applications.select_related("entrepreneur__user", "business")
            .order_by("-computed_score", "-submitted_at")
        )
        rubric = getattr(programme, "rubric", None)
        ctx = {
            "programme": programme,
            "form_fields": form_fields,
            "applications": applications,
            "application_count": applications.count(),
            "rubric": rubric,
            "rubric_sections": rubric.sections.all() if rubric else [],
            "is_external": programme.call_type == Programme.CallType.EXTERNAL,
        }
        return render(request, self.template_name, ctx)
