from django.conf import settings
from rest_framework import viewsets, permissions
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView

from core.permissions import IsAdmin
from core.responses import success_response, error_response

from .models import PipelineStage, JobApplication, PipelineLog
from .serializers import PipelineStageSerializer, JobApplicationSerializer, PipelineLogSerializer


class StageListView(APIView):
    """GET /api/v1/pipeline/stages/ — active stages (ordered) for dropdowns & boards."""
    permission_classes = [IsAuthenticated]

    def get(self, request):
        qs = PipelineStage.objects.filter(is_active=True).order_by("sort_order", "id")
        return success_response(PipelineStageSerializer(qs, many=True).data)


class AdminStageViewSet(viewsets.ModelViewSet):
    """Admin CRUD for the stage master. /api/v1/pipeline/admin/stages/ (ADMIN only)."""
    queryset = PipelineStage.objects.all().order_by("sort_order", "id")
    serializer_class = PipelineStageSerializer
    permission_classes = [IsAdmin]


class JobApplicationViewSet(viewsets.ModelViewSet):
    """Candidate ↔ JD pipeline entries. /api/v1/pipeline/applications/

    Filters: ?job=<id> or ?candidate=<id>.
    Move a candidate along the pipeline with PATCH { "stage": <stage_id> }.
    """
    serializer_class = JobApplicationSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        qs = JobApplication.objects.select_related("candidate", "job", "stage")
        # Scope FIRST, so ?job= / ?candidate= can only ever narrow what the
        # caller is already entitled to — never widen it. Without this a
        # recruiter could read another recruiter's pipeline (and the candidate
        # names in it) just by passing an arbitrary id.
        qs = self._scope(qs)
        job = self.request.query_params.get("job")
        candidate = self.request.query_params.get("candidate")
        if job:
            qs = qs.filter(job_id=job)
        if candidate:
            qs = qs.filter(candidate_id=candidate)
        return qs.order_by("stage__sort_order", "-created_at")

    def _scope(self, qs):
        from apps.candidates.access import is_privileged

        user = self.request.user
        if is_privileged(user):
            return qs
        role = (getattr(user, "role", "") or "").upper()
        if role == "CANDIDATE":
            return qs.filter(candidate__user=user)
        # Recruiters: only applications against JDs assigned to them.
        return qs.filter(job__recruiter_assignments__recruiter=user).distinct()

    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)

    def perform_update(self, serializer):
        old_instance = self.get_object()
        old_stage = old_instance.stage
        old_activity = old_instance.activity
        old_remark = old_instance.remark

        new_instance = serializer.save()
        new_stage = new_instance.stage
        new_activity = new_instance.activity
        new_remark = new_instance.remark

        changes = []
        if old_stage != new_stage:
            old_name = old_stage.name if old_stage else "None"
            new_name = new_stage.name if new_stage else "None"
            changes.append(f"Status changed from '{old_name}' to '{new_name}'")
        if old_activity != new_activity:
            changes.append(f"Activity set to '{new_activity or 'None'}'")
        if old_remark != new_remark:
            changes.append(f"Remark updated to '{new_remark or 'None'}'")

        if changes or old_stage != new_stage:
            from .models import PipelineLog
            user = self.request.user if (self.request and self.request.user and self.request.user.is_authenticated) else None
            PipelineLog.objects.create(
                application=new_instance,
                candidate=new_instance.candidate,
                job=new_instance.job,
                performed_by=user,
                status_name=new_stage.name if new_stage else "",
                activity=new_activity or "",
                remark=new_remark or "",
                changes_summary=" | ".join(changes) if changes else "Updated details",
            )

        if old_stage != new_stage:
            from apps.audit_logs.services import log_activity
            log_activity(
                self.request.user,
                "STAGE_UPDATED",
                f"Moved candidate {new_instance.candidate} to stage {new_stage.name if new_stage else 'None'} for JD: {new_instance.job.title}",
                request=self.request
            )
            # Notify assigned recruiters
            stage_event_map = {
                "submitted": ("candidate_submitted", "Candidate Submitted", "has been submitted to client"),
                "selected": ("candidate_shortlisted", "Candidate Shortlisted", "has been shortlisted by client"),
                "offered": ("offer_released", "Offer Released", "has been offered"),
            }
            if new_stage and new_stage.code in stage_event_map:
                event_type, title, action_desc = stage_event_map[new_stage.code]
                recruiters = new_instance.job.assigned_recruiters.all()
                for r_user in recruiters:
                    from apps.notifications.services import send_notification
                    send_notification(
                        user=r_user,
                        title=title,
                        message=f"Candidate {new_instance.candidate.first_name} {new_instance.candidate.last_name} {action_desc} for job {new_instance.job.title}.",
                        type_code=event_type,
                        metadata={
                            "jd_id": new_instance.job.id,
                            "jd_title": new_instance.job.title,
                            "candidate_id": new_instance.candidate.id,
                            "candidate_name": f"{new_instance.candidate.first_name} {new_instance.candidate.last_name}"
                        }
                    )

    @action(detail=False, methods=["post"], url_path="bulk-assign")
    def bulk_assign(self, request):
        """POST /pipeline/applications/bulk-assign/
        Body: { "job": <jd_id>, "candidate_ids": [1, 2, ...] }
        Assigns multiple candidates to one JD in a single call.
        Already-assigned candidates are skipped (unique_together on candidate+job).
        """
        job_id = request.data.get("job")
        candidate_ids = request.data.get("candidate_ids") or []
        if not job_id or not isinstance(candidate_ids, list) or not candidate_ids:
            return success_response(
                {"created": [], "skipped": []},
                "Pass job and a non-empty candidate_ids list.",
            )

        # Only Published JDs can have a candidate pipeline.
        from apps.jobs.models import JobDescription
        job = JobDescription.objects.filter(id=job_id).first()
        if not job:
            return error_response("Job not found.", status_code=404)
        if job.status != "Published":
            return error_response(
                f"Candidates can only be added to a Published job. This JD is {job.status}.",
                status_code=400,
            )

        default_stage = (
            PipelineStage.objects.filter(is_active=True).order_by("sort_order", "id").first()
        )
        existing = set(
            JobApplication.objects.filter(
                job_id=job_id, candidate_id__in=candidate_ids
            ).values_list("candidate_id", flat=True)
        )
        created, skipped = [], list(existing)
        for cid in candidate_ids:
            if cid in existing:
                continue
            app = JobApplication.objects.create(
                job_id=job_id, candidate_id=cid, stage=default_stage,
                created_by=request.user,
            )
            created.append(cid)
        return success_response(
            {"created": created, "skipped": skipped},
            f"{len(created)} candidate(s) assigned, {len(skipped)} already assigned.",
        )

    @action(detail=False, methods=["get"], url_path="board")
    def board(self, request):
        """GET /pipeline/applications/board/?job=<id> — candidates grouped by stage."""
        job = request.query_params.get("job")
        if not job:
            return success_response([], "Pass ?job=<id>")
        apps_by_stage = {}
        for app in JobApplication.objects.filter(job_id=job).select_related("candidate", "stage"):
            key = app.stage_id
            cand = app.candidate
            # Self-applied = the candidate created their own application (careers portal).
            self_applied = bool(app.created_by_id and cand.user_id and app.created_by_id == cand.user_id)
            apps_by_stage.setdefault(key, []).append({
                "application_id": app.id,
                "candidate_id": app.candidate_id,
                "candidate_name": f"{cand.first_name} {cand.last_name}".strip(),
                "source": cand.source,
                "self_applied": self_applied,
                "applied_at": app.created_at.isoformat() if app.created_at else None,
            })
        columns = []
        for st in PipelineStage.objects.filter(is_active=True).order_by("sort_order", "id"):
            columns.append({
                "stage_id": st.id, "stage": st.name, "code": st.code, "outcome": st.outcome,
                "candidates": apps_by_stage.get(st.id, []),
            })
        return success_response(columns)


class JobRankingView(APIView):
    """AI CV Ranking for a JD (RANK module).
    POST /api/v1/pipeline/rank/<job_id>/   {top_n?, weights?}  -> run ranking
    GET  /api/v1/pipeline/rank/<job_id>/                       -> latest run results
    """
    permission_classes = [IsAuthenticated]

    def post(self, request, job_id):
        # RBAC: running a ranking requires the dedicated "Rank Candidates"
        # permission (or the ADMIN role). Enforced server-side so the action
        # can't be triggered via a direct API call / URL by users who lack it.
        role = (getattr(request.user, "role", "") or "").upper()
        if not (role == "ADMIN" or request.user.has_perm("pipeline.rank_candidates")):
            return error_response("You do not have permission to rank candidates.", status_code=403)

        from apps.jobs.models import JobDescription
        from .ranking_service import rank_job_candidates
        job = JobDescription.objects.filter(id=job_id).first()
        if not job:
            return error_response("Job not found.", status_code=404)
        top_n = int(request.data.get("top_n") or 5)
        weights = request.data.get("weights") or None
        result = rank_job_candidates(job, request.user, top_n=top_n, weights=weights)
        if not result.get("ok"):
            return error_response(result.get("error", "Ranking failed."), status_code=400)
        return success_response(result, f"Ranked {result['ranked']} candidate(s).")

    def get(self, request, job_id):
        from .models import RankRun
        run = RankRun.objects.filter(job_id=job_id).order_by("-created_at").first()
        if not run:
            return success_response({"run": None, "scores": []})
        scores = run.scores.select_related("candidate", "application").all()
        data = [{
            "id": s.id,
            "candidate_id": s.candidate_id,
            "candidate_name": f"{s.candidate.first_name} {s.candidate.last_name}".strip(),
            "candidate_email": s.candidate.email or "",
            "candidate_mobile": s.candidate.phone_number or "",
            "candidate_source": s.candidate.source,
            "overall_score": s.overall_score,
            "effective_score": s.effective_score,
            "parameter_scores": s.parameter_scores,
            "rationale": s.rationale,
            "is_top": s.is_top,
            "manual_override": s.manual_override,
            "override_reason": s.override_reason,
            # Screening-email status (per candidate per JD). Read-only here.
            "email_status": (s.application.email_status if s.application else "NOT_SENT"),
            "email_sent_at": (s.application.email_sent_at.isoformat()
                              if s.application and s.application.email_sent_at else None),
        } for s in sorted(scores, key=lambda x: x.effective_score, reverse=True)]
        return success_response({
            "run": {
                "id": run.id, "created_at": run.created_at.isoformat(),
                "provider": run.provider_name, "top_n": run.top_n,
                "weights": run.weights, "candidate_count": run.candidate_count,
                "run_by": (run.run_by.full_name or run.run_by.email) if run.run_by else None,
            },
            "scores": data,
        })


class ScreeningEmailView(APIView):
    """Send a screening-result email to the SELECTED candidates of a JD only.

    POST /api/v1/pipeline/rank/<job_id>/send-emails/   {candidate_ids: [..]}

    Emails go only to the explicitly selected candidates (never automatically by
    rank), and each candidate's per-JD email status is recorded on their
    JobApplication. The AI ranking, scores and every other pipeline behaviour
    are untouched. Same RBAC as running a ranking (pipeline.rank_candidates).
    """
    permission_classes = [IsAuthenticated]

    def post(self, request, job_id):
        from django.utils import timezone
        from apps.jobs.models import JobDescription
        from apps.notifications.services import send_email_notification
        from .models import JobApplication

        role = (getattr(request.user, "role", "") or "").upper()
        if not (role == "ADMIN" or request.user.has_perm("pipeline.rank_candidates")):
            return error_response("You do not have permission to send screening emails.", status_code=403)

        job = JobDescription.objects.filter(id=job_id).first()
        if not job:
            return error_response("Job not found.", status_code=404)

        candidate_ids = request.data.get("candidate_ids") or []
        if not isinstance(candidate_ids, list) or not candidate_ids:
            return error_response("Select at least one candidate to email.", status_code=400)
        try:
            candidate_ids = [int(c) for c in candidate_ids]
        except (TypeError, ValueError):
            return error_response("Invalid candidate id list.", status_code=400)

        # Only candidates actually in THIS JD's pipeline can be emailed here —
        # prevents emailing arbitrary candidates via a crafted request.
        applications = (
            JobApplication.objects.filter(job=job, candidate_id__in=candidate_ids)
            .select_related("candidate")
        )

        client_name = job.client.name if job.client else ""
        sent = failed = 0
        results = []
        for app in applications:
            cand = app.candidate
            name = f"{cand.first_name} {cand.last_name}".strip() or (cand.email or "Candidate")
            email = (cand.email or "").strip()
            if not email:
                app.email_status = JobApplication.EmailStatus.FAILED
                app.save(update_fields=["email_status", "updated_at"])
                failed += 1
                results.append({"candidate_id": cand.id, "status": "FAILED", "reason": "No email address on file."})
                continue
            at_company = f" at {client_name}" if client_name else ""
            ok = send_email_notification(
                to_emails=email,
                subject=f"You've Been Shortlisted — {job.title}{at_company}",
                heading=f"Congratulations, {name}!",
                intro=(
                    f"We're pleased to let you know that your profile has successfully cleared the "
                    f"initial AI-powered screening for the {job.title} role{at_company}. This is a "
                    "great first step, and we're excited to have you move further in our process."
                ),
                details=[("Position", job.title)] + ([("Company", client_name)] if client_name else []),
                outro=(
                    "Please note that this is not yet a final selection — your profile has been "
                    "shortlisted and passed on to our hiring team for a closer review. Our "
                    "recruitment team will reach out to you directly if you're selected to move "
                    "forward to the next stage of the hiring process. We appreciate the time and "
                    "effort you've invested in your application, and we wish you the very best as "
                    "the process continues. Thank you for your patience."
                ),
            )
            if ok:
                app.email_status = JobApplication.EmailStatus.SENT
                app.email_sent_at = timezone.now()
                app.save(update_fields=["email_status", "email_sent_at", "updated_at"])
                sent += 1
                results.append({"candidate_id": cand.id, "status": "SENT"})
            else:
                app.email_status = JobApplication.EmailStatus.FAILED
                app.save(update_fields=["email_status", "updated_at"])
                failed += 1
                results.append({"candidate_id": cand.id, "status": "FAILED"})

        msg = f"Email sent to {sent} candidate(s)." + (f" {failed} could not be sent." if failed else "")
        return success_response({"sent": sent, "failed": failed, "results": results}, msg)


class RankScoreOverrideView(APIView):
    """RANK-006: manual override of a rank score with a MANDATORY reason.
    PATCH /api/v1/pipeline/rank-score/<id>/  {score, reason}
    """
    permission_classes = [IsAuthenticated]

    def patch(self, request, pk):
        from .models import RankScore
        rs = RankScore.objects.filter(id=pk).first()
        if not rs:
            return error_response("Rank score not found.", status_code=404)
        reason = (request.data.get("reason") or "").strip()
        score = request.data.get("score")
        if not reason:
            return error_response("A reason is required to override the score.", status_code=400)
        try:
            score = int(score)
            assert 0 <= score <= 100
        except (TypeError, ValueError, AssertionError):
            return error_response("Score must be an integer between 0 and 100.", status_code=400)
        rs.manual_override = score
        rs.override_reason = reason
        rs.overridden_by = request.user
        rs.save(update_fields=["manual_override", "override_reason", "overridden_by"])
        if rs.application_id:
            rs.application.score = score
            rs.application.save(update_fields=["score", "updated_at"])
        return success_response({"id": rs.id, "effective_score": rs.effective_score}, "Score overridden.")


class RankParameterViewSet(viewsets.ModelViewSet):
    """RANK-003: admin CRUD for scoring parameters (no code change to add new ones).
    /api/v1/pipeline/rank-parameters/"""
    from .models import RankParameter as _RP
    queryset = _RP.objects.all()
    permission_classes = [IsAuthenticated]

    def get_serializer_class(self):
        from rest_framework import serializers
        from .models import RankParameter

        class RankParameterSerializer(serializers.ModelSerializer):
            class Meta:
                model = RankParameter
                fields = ["id", "key", "label", "description", "weight", "is_active", "sort_order"]
        return RankParameterSerializer

    def list(self, request, *args, **kwargs):
        data = self.get_serializer(self.get_queryset().order_by("sort_order", "id"), many=True).data
        return success_response(data)

    def create(self, request, *args, **kwargs):
        s = self.get_serializer(data=request.data)
        s.is_valid(raise_exception=True); s.save()
        return success_response(s.data, "Parameter added")

    def update(self, request, *args, **kwargs):
        s = self.get_serializer(self.get_object(), data=request.data, partial=kwargs.pop("partial", False))
        s.is_valid(raise_exception=True); s.save()
        return success_response(s.data, "Parameter updated")

    def destroy(self, request, *args, **kwargs):
        self.get_object().delete()
        return success_response(None, "Parameter deleted")


class InterviewFeedbackViewSet(viewsets.ModelViewSet):
    """R2-003/004: structured interview feedback. /api/v1/pipeline/feedback/?application=<id>&candidate=<id>"""
    permission_classes = [IsAuthenticated]

    def get_serializer_class(self):
        from .serializers import InterviewFeedbackSerializer
        return InterviewFeedbackSerializer

    def get_queryset(self):
        from .models import InterviewFeedback
        qs = InterviewFeedback.objects.select_related("application", "created_by").all()
        app_id = self.request.query_params.get("application")
        cand = self.request.query_params.get("candidate")
        if app_id:
            qs = qs.filter(application_id=app_id)
        if cand:
            qs = qs.filter(application__candidate_id=cand)
        return qs

    def list(self, request, *args, **kwargs):
        return success_response(self.get_serializer(self.get_queryset(), many=True).data)

    def perform_create(self, serializer):
        serializer.save(created_by=self.request.user)

    def create(self, request, *args, **kwargs):
        s = self.get_serializer(data=request.data)
        s.is_valid(raise_exception=True)
        self.perform_create(s)
        return success_response(s.data, "Feedback saved")

    def update(self, request, *args, **kwargs):
        s = self.get_serializer(self.get_object(), data=request.data, partial=kwargs.pop("partial", False))
        s.is_valid(raise_exception=True); s.save()
        return success_response(s.data, "Feedback updated")

    def destroy(self, request, *args, **kwargs):
        self.get_object().delete()
        return success_response(None, "Feedback deleted")


class InterviewScheduleViewSet(viewsets.ModelViewSet):
    """R2-001/002/006: schedule interviews + reschedule/cancel with reason + .ics invites."""
    permission_classes = [IsAuthenticated]

    def get_serializer_class(self):
        from .serializers import InterviewScheduleSerializer
        return InterviewScheduleSerializer

    def get_queryset(self):
        from .models import InterviewSchedule
        qs = InterviewSchedule.objects.select_related("application", "application__candidate").all()
        app_id = self.request.query_params.get("application")
        cand = self.request.query_params.get("candidate")
        if app_id:
            qs = qs.filter(application_id=app_id)
        if cand:
            qs = qs.filter(application__candidate_id=cand)
        return qs

    def list(self, request, *args, **kwargs):
        return success_response(self.get_serializer(self.get_queryset(), many=True).data)

    def _send_invite(self, sched):
        from .ics import build_ics, send_invite
        if not sched.scheduled_at:
            return False, "No date/time set."
        cand = sched.application.candidate
        cand_email = cand.email or (cand.user.email if cand.user_id else None)
        summary = f"Interview: {cand.first_name} {cand.last_name} — {sched.application.job.title}"
        ics = build_ics(
            summary, sched.scheduled_at, sched.duration_minutes, sched.location,
            settings.DEFAULT_FROM_EMAIL, [cand_email, sched.interviewer_email],
            description=f"Round: {sched.round}. Location: {sched.location or 'TBD'}",
        )
        body = f"You are scheduled for an interview.\n\nRole: {sched.application.job.title}\nWhen: {sched.scheduled_at}\nWhere: {sched.location or 'TBD'}\n\nA calendar invite is attached."
        return send_invite(summary, body, [cand_email, sched.interviewer_email], ics)

    def create(self, request, *args, **kwargs):
        s = self.get_serializer(data=request.data)
        s.is_valid(raise_exception=True)
        sched = s.save(created_by=request.user)
        ok, err = self._send_invite(sched)
        msg = "Interview scheduled — invite sent." if ok else f"Interview scheduled (invite not sent: {err})"
        return success_response(self.get_serializer(sched).data, msg)

    @action(detail=True, methods=["post"], url_path="reschedule")
    def reschedule(self, request, pk=None):
        sched = self.get_object()
        new_dt = request.data.get("scheduled_at")
        reason = (request.data.get("reason") or "").strip()
        if not new_dt:
            return error_response("New date/time is required.", status_code=400)
        if not reason:
            return error_response("A reason is required to reschedule.", status_code=400)
        sched.scheduled_at = new_dt
        sched.status = "RESCHEDULED"
        sched.reason = reason
        sched.save()
        ok, err = self._send_invite(sched)
        return success_response(self.get_serializer(sched).data,
                                "Rescheduled — new invite sent." if ok else f"Rescheduled (invite not sent: {err})")

    @action(detail=True, methods=["post"], url_path="cancel")
    def cancel(self, request, pk=None):
        sched = self.get_object()
        reason = (request.data.get("reason") or "").strip()
        if not reason:
            return error_response("A reason is required to cancel.", status_code=400)
        sched.status = "CANCELLED"
        sched.reason = reason
        sched.save()
        return success_response(self.get_serializer(sched).data, "Interview cancelled.")


class RoundListView(APIView):
    """R2-001/R2-005: filtered candidate list for the next round.
    GET /api/v1/pipeline/round-list/?job=<id>&round1=SCREENED_IN
        or ?job=<id>&recommendation=YES,STRONG_YES  (round-2 outcomes)
    """
    permission_classes = [IsAuthenticated]

    def get(self, request):
        job = request.query_params.get("job")
        r1 = request.query_params.get("round1")
        rec = request.query_params.get("recommendation")
        qs = JobApplication.objects.select_related("candidate", "job")
        if job:
            qs = qs.filter(job_id=job)
        if r1:
            qs = qs.filter(round1_outcome__in=[x.strip() for x in r1.split(",") if x.strip()])
        if rec:
            recs = [x.strip() for x in rec.split(",") if x.strip()]
            qs = qs.filter(feedbacks__recommendation__in=recs).distinct()
        data = [{
            "application_id": a.id,
            "candidate_id": a.candidate_id,
            "candidate_name": f"{a.candidate.first_name} {a.candidate.last_name}".strip(),
            "job_title": a.job.title,
            "round1_outcome": a.round1_outcome,
            "score": a.score,
        } for a in qs]
        return success_response(data)


class ShortlistCreateView(APIView):
    """R3-001: create a customer-shareable shortlist for a JD.
    POST /api/v1/pipeline/shortlists/  {job, title, note, application_ids:[...]}"""
    permission_classes = [IsAuthenticated]

    def post(self, request):
        from apps.jobs.models import JobDescription
        from .models import Shortlist, ShortlistItem
        job_id = request.data.get("job")
        app_ids = request.data.get("application_ids") or []
        job = JobDescription.objects.filter(id=job_id).first()
        if not job:
            return error_response("Job not found.", status_code=404)
        sl = Shortlist.objects.create(
            job=job, token=Shortlist.new_token(),
            title=request.data.get("title") or f"Shortlist — {job.title}",
            note=request.data.get("note") or "", created_by=request.user,
        )
        for aid in app_ids:
            app = JobApplication.objects.filter(id=aid, job=job).first()
            if app:
                ShortlistItem.objects.get_or_create(shortlist=sl, application=app)
        base = (getattr(settings, "CORS_ALLOWED_ORIGINS", ["http://localhost:3000"]) or ["http://localhost:3000"])[0]
        return success_response({"id": sl.id, "token": sl.token, "url": f"{base}/shortlist/{sl.token}"}, "Shortlist created")

    def get(self, request):
        # list shortlists for a job
        from .models import Shortlist
        job_id = request.query_params.get("job")
        qs = Shortlist.objects.all()
        if job_id:
            qs = qs.filter(job_id=job_id)
        base = (getattr(settings, "CORS_ALLOWED_ORIGINS", ["http://localhost:3000"]) or ["http://localhost:3000"])[0]
        return success_response([{
            "id": s.id, "token": s.token, "title": s.title, "is_active": s.is_active,
            "url": f"{base}/shortlist/{s.token}", "items": s.items.count(),
            "created_at": s.created_at.isoformat(),
        } for s in qs])


class PublicShortlistView(APIView):
    """R3-002/005: public customer portal (token) — read candidates, pick, comment, set final result."""
    permission_classes = [permissions.AllowAny]

    def _get(self, token):
        from .models import Shortlist
        return Shortlist.objects.filter(token=token, is_active=True).first()

    def get(self, request, token):
        sl = self._get(token)
        if not sl:
            return error_response("Shortlist not found or inactive.", status_code=404)
        items = []
        for it in sl.items.select_related("application__candidate", "application__job"):
            c = it.application.candidate
            items.append({
                "item_id": it.id,
                "candidate_name": f"{c.first_name} {c.last_name}".strip(),
                "current_role": c.current_role, "current_company": c.current_company,
                "experience": str(c.total_experience or ""), "skills": list(c.skills.values_list("name", flat=True)),
                "score": it.application.score,
                "picked": it.picked, "customer_comment": it.customer_comment, "final_result": it.final_result,
            })
        return success_response({
            "title": sl.title, "note": sl.note, "job_title": sl.job.title, "items": items,
        })

    def post(self, request, token):
        """Customer updates one item: {item_id, picked?, comment?, final_result?}"""
        from .models import ShortlistItem
        sl = self._get(token)
        if not sl:
            return error_response("Shortlist not found or inactive.", status_code=404)
        it = ShortlistItem.objects.filter(id=request.data.get("item_id"), shortlist=sl).first()
        if not it:
            return error_response("Item not found.", status_code=404)
        if "picked" in request.data:
            it.picked = bool(request.data["picked"])
        if "comment" in request.data:
            it.customer_comment = request.data.get("comment") or ""
        fr = request.data.get("final_result")
        if fr in dict(ShortlistItem.Result.choices):
            it.final_result = fr
            # R3-006: on selection, move candidate to Offer stage
            if fr == "SELECTED":
                offer = PipelineStage.objects.filter(is_active=True, name__icontains="offer").first()
                if offer:
                    it.application.stage = offer
                    it.application.save(update_fields=["stage", "updated_at"])
                    from apps.audit_logs.services import log_activity
                    log_activity(
                        request.user,
                        "STAGE_UPDATED",
                        f"Moved candidate {it.application.candidate} to stage {offer.name} (Customer Selected) for JD: {it.application.job.title}",
                        request=request
                    )
                    # Notify assigned recruiters
                    recruiters = it.application.job.assigned_recruiters.all()
                    for r_user in recruiters:
                        from apps.notifications.services import send_notification
                        send_notification(
                            user=r_user,
                            title="Offer Released",
                            message=f"Candidate {it.application.candidate.first_name} {it.application.candidate.last_name} has been offered (Customer Selected) for job {it.application.job.title}.",
                            type_code="offer_released",
                            metadata={
                                "jd_id": it.application.job.id,
                                "jd_title": it.application.job.title,
                                "candidate_id": it.application.candidate.id,
                                "candidate_name": f"{it.application.candidate.first_name} {it.application.candidate.last_name}"
                            }
                        )
        it.save()
        return success_response({"item_id": it.id, "final_result": it.final_result, "picked": it.picked}, "Updated")


class FinalTranscriptNotesView(APIView):
    """R3-004: turn a final-interview transcript into structured notes via the LLM,
    saved as FINAL-round InterviewFeedback. POST /pipeline/final-notes/ {application, transcript}"""
    permission_classes = [IsAuthenticated]

    def post(self, request, *args, **kwargs):
        from apps.llm.service import call_llm
        from .models import InterviewFeedback
        app_id = request.data.get("application")
        transcript = (request.data.get("transcript") or "").strip()
        app = JobApplication.objects.filter(id=app_id).select_related("candidate", "job").first()
        if not app:
            return error_response("Application not found.", status_code=404)
        if not transcript:
            return error_response("Transcript text is required.", status_code=400)
        prompt = (
            "Summarise this final interview transcript into strict JSON:\n"
            '{"overall": <0-100>, "strengths": "...", "weaknesses": "...", '
            '"recommendation": "STRONG_YES|YES|MAYBE|NO|STRONG_NO", "notes": "3-4 sentence summary"}\n\n'
            f"Role: {app.job.title}\nCandidate: {app.candidate.first_name} {app.candidate.last_name}\n\n"
            f"TRANSCRIPT:\n{transcript[:6000]}"
        )
        r = call_llm(prompt, purpose="final_notes", user=request.user)
        if not r["ok"]:
            return error_response(f"AI notes failed: {r['error']}", status_code=400)
        import json, re
        m = re.search(r"\{.*\}", r["text"], re.DOTALL)
        data = {}
        if m:
            try:
                data = json.loads(m.group(0))
            except Exception:
                pass
        fb = InterviewFeedback.objects.create(
            application=app, round="FINAL", interviewer="AI Transcript Bot",
            overall_score=int(data.get("overall") or 0) if str(data.get("overall") or "").isdigit() else None,
            strengths=str(data.get("strengths") or ""), weaknesses=str(data.get("weaknesses") or ""),
            recommendation=str(data.get("recommendation") or "") if data.get("recommendation") in
                {"STRONG_YES","YES","MAYBE","NO","STRONG_NO"} else "",
            notes=str(data.get("notes") or r["text"][:500]), created_by=request.user,
        )
        from .serializers import InterviewFeedbackSerializer
        return success_response(InterviewFeedbackSerializer(fb).data, "Structured notes saved from transcript.")


class PipelineLogViewSet(viewsets.ReadOnlyModelViewSet):
    """GET /api/v1/pipeline/logs/?candidate=<id> or ?application=<id>"""
    serializer_class = PipelineLogSerializer
    permission_classes = [IsAuthenticated]

    def get_queryset(self):
        qs = PipelineLog.objects.select_related("performed_by", "job", "candidate", "application")
        candidate = self.request.query_params.get("candidate")
        application = self.request.query_params.get("application")
        if candidate:
            qs = qs.filter(candidate_id=candidate)
        if application:
            qs = qs.filter(application_id=application)
        return qs.order_by("-created_at")
