import json

from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import PermissionDenied, ValidationError
from django.db.models import Q
from django.utils import timezone
from django.utils.text import slugify
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.http import url_has_allowed_host_and_scheme
from django.views import View
from django.views.generic import DetailView, ListView

from apps.core.lists.mixins import CsvExportMixin, SortableListMixin
from apps.core.audit.rep_helpers import action_create, action_update, audit_rep
from apps.alumni.careers.access import user_can_view_job_application, user_can_view_job_listing_applications
from apps.alumni.careers.forms import (
    CVProfileForm,
    EmployerListingReviewForm,
    JobApplicationForm,
    JobApplicationStatusForm,
    JobListingForm,
)
from apps.alumni.careers.models import (
    CVProfile,
    CareerResource,
    CareerResourceAccessLog,
    JobApplication,
    JobListing,
    SavedJob,
)
from apps.alumni.careers.services import (
    apply_for_job,
    build_sections_initial,
    cv_dict_has_substance,
    decide_application,
    get_or_create_cv,
    html_meaningfully_filled,
    parse_cv_sections_json,
    resolve_cv_display_for_recruiter,
    sync_cv_sections,
)
from apps.core.permissions.pending_review import (
    WRITE_METHODS as _PENDING_WRITE_METHODS,
    _is_pending as _is_pending_review,
    _redirect_with_flash as _pending_redirect,
)
from apps.core.permissions.roles import UserRole
from apps.alumni.careers.permissions import (
    CAREER_STAFF_ROLE_VALUES,
    POST_JOB_LISTING_PERM,
    CareerManageMixin,
)


# FRREP-TS003 — employers / partners post jobs into the approval queue.
# Allows REP staff + PARTNER role; learners and other roles are blocked.
class _PartnerOrManagerMixin(LoginRequiredMixin):
    """Gate for posting / editing job listings.

    The SRS lists "Employer / Partner" as the canonical actor for SP8 job
    posting. Partners save into the PENDING queue (per the body of
    ``JobCreateView.post``) so admin review still gates publication. Holders
    of the explicit ``rep_career.post_job_listing`` permission are also
    allowed — that is the integration path for guardian-style invitations.
    """

    _allowed_roles = CAREER_STAFF_ROLE_VALUES | {UserRole.PARTNER.value}

    def dispatch(self, request, *args, **kwargs):
        if not request.user.is_authenticated:
            return self.handle_no_permission()
        if (
            request.method in _PENDING_WRITE_METHODS
            and _is_pending_review(request.user)
        ):
            return _pending_redirect(request)
        u = request.user
        if u.is_superuser:
            return super().dispatch(request, *args, **kwargs)
        role = getattr(u, "role", "") or ""
        if role in self._allowed_roles:
            return super().dispatch(request, *args, **kwargs)
        if u.has_perm(POST_JOB_LISTING_PERM):
            return super().dispatch(request, *args, **kwargs)
        raise PermissionDenied


class JobBoardView(LoginRequiredMixin, SortableListMixin, CsvExportMixin, ListView):
    model = JobListing
    template_name = "rep/career/job_board.html"
    context_object_name = "listings"
    paginate_by = 20
    sortable_fields = {
        "title": "title",
        "company": "company",
        "location": "location",
        "sector": "sector",
        "closing": "closes_at",
        "created": "created_at",
    }
    default_sort = "-created_at"
    csv_columns = [
        ("Title", "title"),
        ("Company", "company"),
        ("Location", "location"),
        ("Sector", "sector"),
        ("Job type", "get_job_type_display"),
        ("Closes", "closes_at"),
        ("Posted", "created_at"),
    ]
    csv_filename = "job-board"

    def get_queryset(self):
        qs = JobListing.objects.filter(
            is_active=True,
            listing_status=JobListing.ListingStatus.PUBLISHED,
        )
        q = self.request.GET.get("q", "").strip()
        if q:
            qs = qs.filter(
                Q(title__icontains=q) | Q(company__icontains=q) | Q(location__icontains=q)
            )
        jt = self.request.GET.get("job_type", "").strip()
        if jt:
            qs = qs.filter(job_type=jt)
        sector = self.request.GET.get("sector", "").strip()
        if sector:
            qs = qs.filter(sector__iexact=sector)
        # FRREP-TS002 — country + minimum-qualification filters.
        country = self.request.GET.get("country", "").strip()
        if country:
            qs = qs.filter(country__iexact=country)
        min_qualification = self.request.GET.get("min_qualification", "").strip()
        if min_qualification:
            qs = qs.filter(min_qualification=min_qualification)
        return self.apply_sort(qs)

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["job_type_choices"] = JobListing.JobType.choices
        ctx["min_qualification_choices"] = JobListing._meta.get_field("min_qualification").choices
        published_qs = JobListing.objects.filter(
            is_active=True,
            listing_status=JobListing.ListingStatus.PUBLISHED,
        )
        # Use the canonical sector list, filtered to only sectors that have active listings
        active_sectors = set(
            published_qs.exclude(sector="").values_list("sector", flat=True).distinct()
        )
        ctx["distinct_sectors"] = [
            s for s, _ in JobListing.SECTOR_CHOICES if s in active_sectors
        ]
        # FRREP-TS002 — cheap distinct query, only countries actually posted
        # against right now (avoids offering 195 dead options in the select).
        ctx["distinct_countries"] = sorted(
            c
            for c in published_qs.exclude(country="").values_list("country", flat=True).distinct()
            if c
        )
        u = self.request.user
        if u.is_authenticated:
            ctx["my_applications"] = set(
                JobApplication.objects.filter(applicant=u).values_list("listing_id", flat=True)
            )
            ctx["can_review_applications_for"] = {
                L.pk
                for L in ctx["listings"]
                if user_can_view_job_listing_applications(u, L)
            }
            # FRREP-TS008 — which of the listings on this page the learner
            # has already bookmarked, for the Save/Saved toggle button.
            ctx["my_saved_jobs"] = set(
                SavedJob.objects.filter(user=u).values_list("listing_id", flat=True)
            )
        else:
            ctx["my_applications"] = set()
            ctx["can_review_applications_for"] = set()
            ctx["my_saved_jobs"] = set()
        return ctx


class JobDetailView(LoginRequiredMixin, DetailView):
    model = JobListing
    template_name = "rep/career/job_detail.html"
    context_object_name = "listing"

    def get_queryset(self):
        return JobListing.objects.filter(
            is_active=True,
            listing_status=JobListing.ListingStatus.PUBLISHED,
        )

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["already_applied"] = JobApplication.objects.filter(
            listing=self.object, applicant=self.request.user
        ).exists()
        ctx["form"] = JobApplicationForm()
        can_review = user_can_view_job_listing_applications(self.request.user, self.object)
        ctx["can_view_applications"] = can_review
        ctx["listing_application_count"] = (
            JobApplication.objects.filter(listing=self.object).count() if can_review else 0
        )
        u = self.request.user
        ctx["can_edit_listing"] = u.is_authenticated and (
            u.is_superuser
            or getattr(u, "role", "") == "admin"
            or self.object.posted_by_id == u.id
        )
        # FRREP-TS008 — bookmark state for the Save/Saved button.
        ctx["is_saved"] = u.is_authenticated and SavedJob.objects.filter(
            user=u, listing=self.object
        ).exists()
        return ctx

    def post(self, request, *args, **kwargs):
        listing = self.get_object()
        form = JobApplicationForm(request.POST, request.FILES)
        if form.is_valid():
            try:
                apply_for_job(
                    request.user,
                    listing,
                    form.cleaned_data["cover_letter"],
                    cover_letter_file=form.cleaned_data.get("cover_letter_file"),
                )
                audit_rep(
                    actor=request.user,
                    action=action_create(),
                    target_model="JobApplication",
                    object_id=f"{listing.pk}:{request.user.pk}",
                    object_repr=f"{request.user.email} -> {listing.title}",
                    changes={"listing_id": listing.pk},
                    request=request,
                )
                messages.success(request, "Application submitted.")
                return redirect("rep_career:job_detail", pk=listing.pk)
            except ValidationError as exc:
                messages.error(request, " ".join(exc.messages))
        ctx = self.get_context_data()
        ctx["form"] = form
        return render(request, self.template_name, ctx)


class JobCreateView(_PartnerOrManagerMixin, View):
    def get(self, request):
        form = JobListingForm()
        return render(request, "rep/career/job_create.html", {"form": form})

    def post(self, request):
        form = JobListingForm(request.POST)
        if form.is_valid():
            listing = form.save(commit=False)
            listing.posted_by = request.user
            if not request.user.is_superuser:
                listing.listing_status = JobListing.ListingStatus.PENDING
                listing.is_active = False
            listing.save()
            audit_rep(
                actor=request.user,
                action=action_create(),
                target_model="JobListing",
                object_id=listing.pk,
                object_repr=listing.title,
                changes={"job_type": listing.job_type, "sector": listing.sector},
                request=request,
            )
            if listing.listing_status == JobListing.ListingStatus.PENDING:
                messages.success(request, "Job listing submitted for approval.")
            else:
                messages.success(request, "Job listing created.")
            return redirect("rep_career:job_board")
        return render(request, "rep/career/job_create.html", {"form": form})


class EmployerListingReviewListView(CareerManageMixin, SortableListMixin, CsvExportMixin, ListView):
    template_name = "rep/career/listing_reviews.html"
    context_object_name = "listings"
    sortable_fields = {
        "title": "title",
        "company": "company",
        "created": "created_at",
    }
    default_sort = "-created_at"
    csv_columns = [
        ("Title", "title"),
        ("Company", "company"),
        ("Posted by", lambda L: L.posted_by.email if L.posted_by else ""),
        ("Submitted", "created_at"),
    ]
    csv_filename = "listing-reviews"

    def get_queryset(self):
        return self.apply_sort(
            JobListing.objects.filter(listing_status=JobListing.ListingStatus.PENDING)
        )


class EmployerListingReviewUpdateView(CareerManageMixin, View):
    def get(self, request, pk):
        listing = get_object_or_404(JobListing, pk=pk)
        form = EmployerListingReviewForm(instance=listing)
        return render(request, "rep/career/listing_review_detail.html", {"listing": listing, "form": form})

    def post(self, request, pk):
        listing = get_object_or_404(JobListing, pk=pk)
        previous_status = listing.listing_status
        form = EmployerListingReviewForm(request.POST, instance=listing)
        if form.is_valid():
            row = form.save(commit=False)
            if row.listing_status == JobListing.ListingStatus.PUBLISHED:
                row.is_active = True
            if row.listing_status in (JobListing.ListingStatus.REJECTED, JobListing.ListingStatus.DRAFT):
                row.is_active = False
            row.save()
            # FRREP-TS004 — when a listing flips into PUBLISHED, fire match
            # notifications to learners whose stored CV sector matches.
            if (
                previous_status != JobListing.ListingStatus.PUBLISHED
                and row.listing_status == JobListing.ListingStatus.PUBLISHED
            ):
                _fan_out_job_listing_matches(row)
            messages.success(request, "Listing review decision saved.")
            return redirect("rep_career:listing_reviews")
        return render(request, "rep/career/listing_review_detail.html", {"listing": listing, "form": form})


def _fan_out_job_listing_matches(listing) -> None:
    """FRREP-TS004 — minimal sector-based matcher.

    A richer algorithm (skills, location, alumni cohort) lives behind a P1
    follow-up; this baseline matcher notifies any learner whose CV headline or
    summary mentions the listing sector. Recipients are deduplicated. Email
    delivery uses ``send_notification`` so each learner's per-verb preference
    still applies.
    """
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        sector = (listing.sector or "").strip()
        if not sector:
            return
        candidates = (
            CVProfile.objects.filter(
                Q(headline__icontains=sector) | Q(summary__icontains=sector)
            )
            .select_related("user")
            .distinct()
        )
        portal_action = f"/rep/career/jobs/{listing.pk}/"
        for cv in candidates.iterator(chunk_size=200):
            send_notification(
                cv.user,
                f'A new job in {sector} was posted: "{listing.title}".',
                verb=Notification.Verb.REP_JOB_LISTING_MATCH,
                email_context={
                    "listing_title": listing.title,
                    "company": listing.company,
                    "sector": sector,
                    "action_url": portal_action,
                },
            )
    except Exception:
        import logging as _logging

        _logging.getLogger(__name__).exception(
            "rep.career: job_listing_match fan-out failed listing=%s",
            getattr(listing, "pk", None),
        )


class CVBuilderView(LoginRequiredMixin, View):
    """Build/edit the learner's CV profile."""

    def get(self, request):
        cv = get_or_create_cv(request.user)
        cv = CVProfile.objects.prefetch_related("sections").get(pk=cv.pk)
        form = CVProfileForm(instance=cv)
        return render(
            request,
            "rep/career/cv_builder.html",
            {
                "cv": cv,
                "form": form,
                "sections_initial": build_sections_initial(cv),
            },
        )

    def post(self, request):
        cv = get_or_create_cv(request.user)
        cv = CVProfile.objects.prefetch_related("sections").get(pk=cv.pk)
        form = CVProfileForm(request.POST, request.FILES, instance=cv)
        raw_json = request.POST.get("cv_sections_json", "")

        def sections_for_template() -> dict:
            if raw_json.strip():
                try:
                    data = json.loads(raw_json)
                    if isinstance(data, dict):
                        base = build_sections_initial(cv)
                        for k in base:
                            if k in data:
                                base[k] = data[k]
                        return base
                except json.JSONDecodeError:
                    pass
            return build_sections_initial(cv)

        if not form.is_valid():
            return render(
                request,
                "rep/career/cv_builder.html",
                {"cv": cv, "form": form, "sections_initial": sections_for_template()},
            )

        try:
            payload = parse_cv_sections_json(raw_json)
            sync_cv_sections(cv, payload)
        except ValidationError as exc:
            for msg in exc.messages:
                messages.error(request, msg)
            return render(
                request,
                "rep/career/cv_builder.html",
                {"cv": cv, "form": form, "sections_initial": sections_for_template()},
            )

        form.save()
        audit_rep(
            actor=request.user,
            action=action_update(),
            target_model="CVProfile",
            object_id=cv.pk,
            object_repr=str(cv),
            changes={"is_public": form.cleaned_data.get("is_public", False)},
            request=request,
        )
        messages.success(request, "CV saved.")
        return redirect("rep_career:cv_builder")


class CVPreviewView(LoginRequiredMixin, View):
    """Render the learner's CV as styled HTML for in-page preview.

    Same template as the PDF export, but always served as HTML (no PDF, no
    attachment disposition) so it can be embedded in an iframe inside the
    builder. Lets applicants see exactly what shortlisters will see.
    """

    def get(self, request):
        from django.http import HttpResponse
        from django.template.loader import render_to_string

        cv = get_or_create_cv(request.user)
        cv = CVProfile.objects.prefetch_related("sections").get(pk=cv.pk)
        sections = build_sections_initial(cv)
        html = render_to_string(
            "rep/career/cv_pdf.html",
            {
                "cv": cv,
                "user": request.user,
                "sections": sections,
                "generated_at": timezone.now(),
            },
        )
        return HttpResponse(html, content_type="text/html; charset=utf-8")


class CVDownloadPDFView(LoginRequiredMixin, View):
    """WS5.8 / FRREP-TS006 — render the learner's CV as a PDF.

    Uses WeasyPrint with an HTML fallback (the dedicated PDF template still
    serves as a styled standalone HTML if WeasyPrint isn't installed in the
    environment).
    """

    def get(self, request):
        from django.http import HttpResponse
        from django.template.loader import render_to_string

        cv = get_or_create_cv(request.user)
        cv = CVProfile.objects.prefetch_related("sections").get(pk=cv.pk)
        sections = build_sections_initial(cv)
        ctx = {
            "cv": cv,
            "user": request.user,
            "sections": sections,
            "generated_at": timezone.now(),
        }
        html = render_to_string("rep/career/cv_pdf.html", ctx)
        filename = f"cv-{request.user.email.split('@')[0]}.pdf"
        try:
            from weasyprint import HTML

            pdf = HTML(string=html, base_url=request.build_absolute_uri("/")).write_pdf()
            resp = HttpResponse(pdf, content_type="application/pdf")
            resp["Content-Disposition"] = f'attachment; filename="{filename}"'
        except (OSError, ImportError, RuntimeError):
            # WeasyPrint not available — serve the styled HTML so the user
            # can save-as-PDF from the browser. PRD A2 alt-flow: degrade
            # gracefully rather than 500.
            resp = HttpResponse(html, content_type="text/html; charset=utf-8")
            resp["Content-Disposition"] = (
                f'inline; filename="{filename.replace(".pdf", ".html")}"'
            )
        # WS2.4 / FRREP-TS009 — audit downloads (helps explain "where did my CV
        # go?" support tickets).
        audit_rep(
            actor=request.user,
            action=action_update(),
            target_model="CVProfile",
            object_id=cv.pk,
            object_repr=str(cv),
            changes={"action": "pdf_export"},
            request=request,
        )
        # FRREP-TS006 — confirm the export to the learner's inbox so they have
        # a paper trail of when each CV snapshot left the system.
        try:
            from apps.core.notifications.models import Notification
            from apps.core.notifications.services import send_notification

            send_notification(
                request.user,
                f"Your CV export ({filename}) is ready.",
                verb=Notification.Verb.REP_CV_EXPORT_READY,
                email_context={
                    "cv_filename": filename,
                    "generated_at": timezone.now().isoformat(),
                },
            )
        except Exception:
            import logging as _logging

            _logging.getLogger(__name__).exception("rep.career.cv_export: notification failed")
        return resp


class MyApplicationsView(LoginRequiredMixin, SortableListMixin, CsvExportMixin, ListView):
    template_name = "rep/career/my_applications.html"
    context_object_name = "applications"
    paginate_by = 15
    sortable_fields = {
        "title": "listing__title",
        "company": "listing__company",
        "status": "status",
        "applied": "applied_at",
    }
    default_sort = "-applied_at"
    csv_columns = [
        ("Listing", "listing.title"),
        ("Company", "listing.company"),
        ("Status", "get_status_display"),
        ("Applied", "applied_at"),
    ]
    csv_filename = "my-applications"

    def get_queryset(self):
        return self.apply_sort(
            JobApplication.objects.filter(applicant=self.request.user)
            .select_related("listing")
        )


class JobSaveToggleView(LoginRequiredMixin, View):
    """FRREP-TS008 — bookmark / un-bookmark a job listing.

    A single idempotent POST toggle (rather than separate save/unsave
    endpoints) keeps the job board and job detail templates to one button +
    one form. Redirects back to wherever the request came from (``next``,
    falling back to the referer, falling back to the job board) so it works
    identically from either surface.
    """

    def post(self, request, pk):
        listing = get_object_or_404(JobListing, pk=pk)
        saved_qs = SavedJob.objects.filter(user=request.user, listing=listing)
        if saved_qs.exists():
            saved_qs.delete()
            messages.info(request, "Removed from your saved jobs.")
        else:
            SavedJob.objects.get_or_create(user=request.user, listing=listing)
            messages.success(request, "Saved to your job list.")
        next_url = request.POST.get("next") or request.META.get("HTTP_REFERER")
        if next_url and url_has_allowed_host_and_scheme(
            next_url, allowed_hosts={request.get_host()}, require_https=request.is_secure()
        ):
            return redirect(next_url)
        return redirect("rep_career:job_board")


class MySavedJobsListView(LoginRequiredMixin, SortableListMixin, CsvExportMixin, ListView):
    """FRREP-TS008 — the learner's saved / bookmarked job listings."""

    template_name = "rep/career/my_saved_jobs.html"
    context_object_name = "saved_jobs"
    paginate_by = 20
    sortable_fields = {
        "title": "listing__title",
        "company": "listing__company",
        "saved": "created_at",
    }
    default_sort = "-created_at"
    csv_columns = [
        ("Listing", "listing.title"),
        ("Company", "listing.company"),
        ("Saved", "created_at"),
    ]
    csv_filename = "saved-jobs"

    def get_queryset(self):
        return self.apply_sort(
            SavedJob.objects.filter(user=self.request.user).select_related("listing")
        )

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        u = self.request.user
        ctx["my_applications"] = set(
            JobApplication.objects.filter(applicant=u).values_list("listing_id", flat=True)
        )
        return ctx


class CareerResourceListView(LoginRequiredMixin, SortableListMixin, CsvExportMixin, ListView):
    template_name = "rep/career/resource_library.html"
    context_object_name = "resources"
    paginate_by = 20
    sortable_fields = {
        "title": "title",
        "type": "resource_type",
        "stage": "career_stage",
        "created": "created_at",
    }
    default_sort = "-created_at"
    csv_columns = [
        ("Title", "title"),
        ("Type", "get_resource_type_display"),
        ("Career stage", "career_stage"),
        ("Tags", "tags"),
        ("Published", "is_published"),
        ("Created", "created_at"),
    ]
    csv_filename = "career-resources"

    def get_queryset(self):
        qs = CareerResource.objects.filter(is_published=True)
        q = self.request.GET.get("q", "").strip()
        if q:
            qs = qs.filter(Q(title__icontains=q) | Q(tags__icontains=q) | Q(career_stage__icontains=q))
        stage = self.request.GET.get("stage", "").strip()
        if stage:
            qs = qs.filter(career_stage__iexact=stage)
        return self.apply_sort(qs)


class CareerResourceDetailView(LoginRequiredMixin, DetailView):
    model = CareerResource
    template_name = "rep/career/resource_detail.html"
    context_object_name = "resource"
    slug_field = "slug"
    slug_url_kwarg = "slug"

    def get_queryset(self):
        return CareerResource.objects.filter(is_published=True)

    def get(self, request, *args, **kwargs):
        self.object = self.get_object()
        CareerResourceAccessLog.objects.create(resource=self.object, user=request.user)
        context = self.get_context_data(object=self.object)
        return self.render_to_response(context)


class CareerResourceCreateView(CareerManageMixin, View):
    def get(self, request):
        return render(request, "rep/career/resource_create.html")

    def post(self, request):
        title = (request.POST.get("title") or "").strip()
        if not title:
            messages.error(request, "Title is required.")
            return render(request, "rep/career/resource_create.html")
        base = slugify(title)[:140] or "resource"
        slug = base
        i = 0
        while CareerResource.objects.filter(slug=slug).exists():
            i += 1
            slug = f"{base}-{i}"
        resource = CareerResource.objects.create(
            title=title,
            slug=slug,
            resource_type=(request.POST.get("resource_type") or CareerResource.ResourceType.ARTICLE),
            body=request.POST.get("body") or "",
            url=request.POST.get("url") or "",
            tags=request.POST.get("tags") or "",
            career_stage=request.POST.get("career_stage") or "",
            is_published=bool(request.POST.get("is_published")),
            created_by=request.user,
        )
        messages.success(request, "Career resource saved.")
        return redirect("rep_career:resource_detail", slug=resource.slug)


class JobApplicationsListView(LoginRequiredMixin, SortableListMixin, CsvExportMixin, ListView):
    """Recruiter / poster: applications received for one job listing."""

    template_name = "rep/career/job_applications_list.html"
    context_object_name = "applications"
    paginate_by = 20
    listing = None
    sortable_fields = {
        "applicant": "applicant__email",
        "status": "status",
        "applied": "applied_at",
    }
    default_sort = "-applied_at"
    csv_columns = [
        ("Applicant", "applicant.email"),
        ("Name", lambda a: a.applicant.get_full_name() or ""),
        ("Status", "get_status_display"),
        ("Applied", "applied_at"),
    ]
    csv_filename = "applications"

    def dispatch(self, request, *args, **kwargs):
        self.listing = get_object_or_404(JobListing, pk=kwargs["listing_pk"])
        if not user_can_view_job_listing_applications(request.user, self.listing):
            raise PermissionDenied
        return super().dispatch(request, *args, **kwargs)

    def get_queryset(self):
        return self.apply_sort(
            JobApplication.objects.filter(listing=self.listing)
            .select_related("applicant")
        )

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


class JobApplicationDetailView(LoginRequiredMixin, DetailView):
    """Single application with CV data (snapshot and/or current profile — see services)."""

    model = JobApplication
    template_name = "rep/career/job_application_detail.html"
    context_object_name = "application"

    def get_queryset(self):
        return JobApplication.objects.select_related("listing", "applicant")

    def get_object(self, queryset=None):
        qs = self.get_queryset()
        obj = get_object_or_404(qs, pk=self.kwargs["pk"], listing_id=self.kwargs["listing_pk"])
        if not user_can_view_job_application(self.request.user, obj):
            raise PermissionDenied
        return obj

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        app = self.object
        ctx["listing"] = app.listing
        cv = get_or_create_cv(app.applicant)
        cv = CVProfile.objects.prefetch_related("sections").get(pk=cv.pk)
        display, mode = resolve_cv_display_for_recruiter(app, cv)
        ctx["cv_display"] = display
        ctx["cv_display_mode"] = mode
        ctx["cv_has_content"] = cv_dict_has_substance(display)
        ctx["cv_profile_updated_at"] = cv.updated_at
        ctx["cover_letter_meaningful"] = html_meaningfully_filled(app.cover_letter)
        ctx["cv_profile_for_applicant"] = cv
        ctx["status_form"] = JobApplicationStatusForm(initial={"status": app.status})
        ctx["can_decide"] = user_can_view_job_listing_applications(self.request.user, app.listing)
        # Applicant certificates were previously sourced from the REP LMS
        # (Certificate/Enrolment). With learning moved to Moodle, this inline
        # credential list is no longer populated from REP; a Moodle-backed
        # verification feed can repopulate it via MEL Moodle DataPoints later.
        ctx["applicant_certificates"] = []
        return ctx


class JobApplicationDecideView(LoginRequiredMixin, View):
    """Recruiter / poster updates an application's status (REP-SP8 / FRREP-TS008).

    Mirrors EmployerListingReviewUpdateView's pattern: form-driven, audited,
    fan-out via signal. Notification + outbox happen inside `decide_application`.
    """

    def post(self, request, listing_pk, pk):
        application = get_object_or_404(
            JobApplication.objects.select_related("listing", "applicant"),
            pk=pk,
            listing_id=listing_pk,
        )
        if not user_can_view_job_listing_applications(request.user, application.listing):
            raise PermissionDenied
        form = JobApplicationStatusForm(request.POST)
        if not form.is_valid():
            messages.error(request, "Pick a status before saving.")
            return redirect(
                "rep_career:job_application_detail",
                listing_pk=listing_pk,
                pk=pk,
            )
        try:
            decide_application(
                application,
                status=form.cleaned_data["status"],
                decided_by=request.user,
                note=form.cleaned_data.get("note") or "",
                request=request,
            )
            messages.success(request, "Application status updated.")
        except ValidationError as exc:
            messages.error(request, " ".join(exc.messages))
        return redirect(
            "rep_career:job_application_detail",
            listing_pk=listing_pk,
            pk=pk,
        )


class JobUpdateView(LoginRequiredMixin, View):
    """Listing owner edits their job posting after creation."""

    def _load(self, request, pk):
        listing = get_object_or_404(JobListing, pk=pk)
        role = getattr(request.user, "role", "") or ""
        is_admin = request.user.is_superuser or role in CAREER_STAFF_ROLE_VALUES
        if not is_admin and listing.posted_by_id != request.user.id:
            raise PermissionDenied
        return listing

    def get(self, request, pk):
        listing = self._load(request, pk)
        form = JobListingForm(instance=listing)
        return render(request, "rep/career/job_create.html", {"form": form, "listing": listing})

    def post(self, request, pk):
        listing = self._load(request, pk)
        form = JobListingForm(request.POST, instance=listing)
        if form.is_valid():
            saved = form.save(commit=False)
            # Editing a published listing returns it to PENDING for re-review
            # only if it's a non-admin author; admins can edit in place.
            _role = getattr(request.user, "role", "") or ""
            _is_admin_edit = request.user.is_superuser or _role in CAREER_STAFF_ROLE_VALUES
            if listing.listing_status == JobListing.ListingStatus.PUBLISHED and not _is_admin_edit:
                saved.listing_status = JobListing.ListingStatus.PENDING
                saved.is_active = False
            saved.save()
            audit_rep(
                actor=request.user,
                action=action_update(),
                target_model="JobListing",
                object_id=listing.pk,
                object_repr=listing.title,
                changes={"edited": True, "new_status": saved.listing_status},
                request=request,
            )
            messages.success(request, "Listing updated.")
            return redirect("rep_career:job_detail", pk=listing.pk)
        return render(request, "rep/career/job_create.html", {"form": form, "listing": listing})
