from __future__ import annotations

import json

from django.contrib import messages
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse, reverse_lazy
from django.utils import timezone
from django.views import View
from django.views.generic import CreateView, DetailView, ListView, UpdateView

from apps.core.permissions.mixins import RoleRequiredMixin
from apps.core.permissions.roles import UserRole
from apps.mel.feedback.forms import (
    CorrectiveActionTransitionForm,
    FeedbackChannelForm,
    FeedbackSubmissionForm,
    FeedbackTriageForm,
)
from apps.mel.tracking.models import CorrectiveAction
from apps.mel.feedback.models import (
    FeedbackChannel,
    FeedbackSubmission,
    SatisfactionScore,
    Survey,
    SurveyAnswer,
    SurveyDispatch,
    SurveyQuestion,
    SurveyQuestionType,
    SurveyResponse,
    SurveyStatus,
    SurveyTrigger,
)
from apps.mel.feedback.services import (
    activate_survey,
    analyse_responses,
    close_survey,
    collect_response,
    create_survey,
    submit_feedback,
    transition_corrective_action,
    triage_feedback,
)

OFFICER_ROLES = (UserRole.MEL_OFFICER, UserRole.ADMIN, UserRole.SYSTEM_ADMIN)
# Read-only survey results (aggregated, anonymised) are also viewable by alumni
# officers — but, for them, only for the alumni-launched impact/tracer surveys
# (see ``_scope_alumni_officer_to_impact``). The write/triage views stay locked
# to OFFICER_ROLES.
RESULTS_ROLES = OFFICER_ROLES + (UserRole.ALUMNI_OFFICER,)
ALUMNI_IMPACT_SURVEY_SLUG_PREFIX = "alumni-impact-"


def _scope_alumni_officer_to_impact(user, survey) -> None:
    """404 if an alumni officer opens a non-alumni survey's results.

    Full MEL/admin officers see everything; an alumni officer (who is not also a
    MEL officer/admin) may only read the results of surveys launched from the
    alumni impact-survey wrapper.
    """
    role = getattr(user, "role", "")
    if user.is_superuser or role in (
        UserRole.MEL_OFFICER,
        UserRole.ADMIN,
        UserRole.SYSTEM_ADMIN,
    ):
        return
    if role == UserRole.ALUMNI_OFFICER and not survey.slug.startswith(
        ALUMNI_IMPACT_SURVEY_SLUG_PREFIX
    ):
        raise Http404()


class FeedbackWindowClosed(Exception):
    """Raised by a token-gated intake when the window is closed/deactivated.

    Carries a respondent-facing message so the view can render a polite
    branded page instead of a raw 404 (D-D3 / D-D7).
    """

    def __init__(self, message: str, *, title: str = "This window is closed"):
        super().__init__(message)
        self.message = message
        self.title = title


def _render_window_closed(request, exc: FeedbackWindowClosed):
    return render(
        request,
        "feedback/window_closed.html",
        {"closed_title": exc.title, "closed_message": exc.message},
        status=200,
    )


def _validate_survey_questions(questions) -> list[str]:
    """Server-side backstop for the survey builder (D-D5).

    Rejects blank question text and choice/rating questions with no usable
    options, mirroring the client-side guard so a crafted POST can't persist
    an unanswerable questionnaire.
    """
    errors: list[str] = []
    for i, q in enumerate(questions or [], start=1):
        text = (q.get("text") or "").strip()
        if not text:
            errors.append(f"Question {i} has no text.")
            continue
        qtype = q.get("type") or SurveyQuestionType.SHORT_ANSWER
        if qtype == SurveyQuestionType.MULTIPLE_CHOICE:
            opts = q.get("options") or []
            if not (isinstance(opts, list) and len(opts) >= 1):
                errors.append(f"Question {i} (multiple choice) needs at least one option.")
        elif qtype == SurveyQuestionType.RATING:
            opts = q.get("options")
            if not (isinstance(opts, dict) and "min" in opts and "max" in opts):
                errors.append(f"Question {i} (rating) needs a min and max value.")
    return errors


# ---------------------------------------------------------------------------
# Public feedback submission (token-gated)
# ---------------------------------------------------------------------------


class PublicFeedbackView(View):
    """Public submission page. Auth not required; access controlled via token."""

    template_name = "feedback/public_submit.html"

    def _get_channel(self, token: str) -> FeedbackChannel:
        # Unknown token → genuine 404. A known-but-closed/deactivated channel
        # renders a polite "window closed" page instead of a raw 404 (D-D7).
        channel = FeedbackChannel.objects.filter(token=token).first()
        if channel is None:
            raise Http404("No such feedback channel.")
        now = timezone.now()
        if not channel.is_active:
            raise FeedbackWindowClosed(
                "This feedback window is no longer open. Thank you for your interest."
            )
        if channel.opens_at and now < channel.opens_at:
            raise FeedbackWindowClosed(
                "This feedback window has not opened yet. Please check back later.",
                title="Not open yet",
            )
        if channel.closes_at and now > channel.closes_at:
            raise FeedbackWindowClosed(
                "This feedback window has closed. Thank you for your interest."
            )
        return channel

    def get(self, request, token: str):
        try:
            channel = self._get_channel(token)
        except FeedbackWindowClosed as exc:
            return _render_window_closed(request, exc)
        form = FeedbackSubmissionForm()
        return render(request, self.template_name, {"channel": channel, "form": form})

    def post(self, request, token: str):
        try:
            channel = self._get_channel(token)
        except FeedbackWindowClosed as exc:
            return _render_window_closed(request, exc)
        form = FeedbackSubmissionForm(request.POST)
        if not form.is_valid():
            return render(request, self.template_name, {"channel": channel, "form": form})

        submit_feedback(
            channel=channel,
            narrative=form.cleaned_data["narrative"],
            submitter_user=request.user if request.user.is_authenticated else None,
            submitter_email=form.cleaned_data.get("submitter_email", ""),
            submitter_name=form.cleaned_data.get("submitter_name", ""),
            rating=form.cleaned_data.get("rating") or None,
            severity=form.cleaned_data["severity"],
        )
        return render(request, "feedback/public_submit_thanks.html", {"channel": channel})


# ---------------------------------------------------------------------------
# Officer inbox
# ---------------------------------------------------------------------------


class FeedbackInboxView(RoleRequiredMixin, ListView):
    allowed_roles = OFFICER_ROLES
    model = FeedbackSubmission
    context_object_name = "submissions"
    paginate_by = 10
    template_name = "feedback/inbox.html"

    def get_queryset(self):
        qs = FeedbackSubmission.objects.select_related("channel").order_by("-created_at")
        status = self.request.GET.get("status", "").strip()
        if status:
            qs = qs.filter(status=status)
        severity = self.request.GET.get("severity", "").strip()
        if severity:
            qs = qs.filter(severity=severity)
        return qs

    def get_context_data(self, **kwargs):
        from django.db.models import Count, Q

        ctx = super().get_context_data(**kwargs)
        # Open submissions = anything not closed; severity triage strip mirrors
        # the compliance-alerts treatment so officers triage by impact.
        ctx["severity_stats"] = FeedbackSubmission.objects.aggregate(
            critical=Count("pk", filter=Q(severity="critical") & ~Q(status="closed")),
            high=Count("pk", filter=Q(severity="high") & ~Q(status="closed")),
            medium=Count("pk", filter=Q(severity="medium") & ~Q(status="closed")),
            low=Count("pk", filter=Q(severity="low") & ~Q(status="closed")),
        )
        return ctx


class FeedbackDetailView(RoleRequiredMixin, DetailView):
    allowed_roles = OFFICER_ROLES
    model = FeedbackSubmission
    context_object_name = "submission"
    template_name = "feedback/detail.html"

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["triage_form"] = FeedbackTriageForm(instance=ctx["submission"])
        if ctx["submission"].corrective_action_id:
            ctx["ca_form"] = CorrectiveActionTransitionForm(
                instance=ctx["submission"].corrective_action,
            )
        # G21 — polymorphic evidence gallery context.
        from django.contrib.contenttypes.models import ContentType

        from apps.mel.indicators.models import EvidenceAttachment

        ct = ContentType.objects.get_for_model(FeedbackSubmission)
        ctx["attachment_ct_id"] = ct.pk
        ctx["attachments"] = list(
            EvidenceAttachment.objects.filter(content_type=ct, object_id=ctx["submission"].pk)
        )
        return ctx


class FeedbackTriageView(RoleRequiredMixin, View):
    allowed_roles = OFFICER_ROLES

    def post(self, request, pk: int):
        submission = get_object_or_404(FeedbackSubmission, pk=pk)
        form = FeedbackTriageForm(request.POST, instance=submission)
        if not form.is_valid():
            messages.error(request, "Triage form invalid.")
            return HttpResponseRedirect(reverse("mel_feedback:detail", kwargs={"pk": pk}))
        submission = form.save()
        triage_feedback(submission, triaged_by=request.user)
        messages.success(request, "Feedback triaged.")
        return HttpResponseRedirect(reverse("mel_feedback:detail", kwargs={"pk": pk}))


class CorrectiveActionTransitionView(RoleRequiredMixin, View):
    """POST-only endpoint to advance the CA linked to a feedback submission."""

    allowed_roles = OFFICER_ROLES

    def post(self, request, pk: int):
        submission = get_object_or_404(FeedbackSubmission, pk=pk)
        if not submission.corrective_action_id:
            messages.error(request, "No corrective action linked to this feedback.")
            return HttpResponseRedirect(reverse("mel_feedback:detail", kwargs={"pk": pk}))
        form = CorrectiveActionTransitionForm(
            request.POST, instance=submission.corrective_action,
        )
        if not form.is_valid():
            messages.error(request, "Corrective action form invalid.")
            return HttpResponseRedirect(reverse("mel_feedback:detail", kwargs={"pk": pk}))
        transition_corrective_action(
            submission.corrective_action,
            new_status=form.cleaned_data["status"],
            actor=request.user,
            action_plan=form.cleaned_data.get("action_plan"),
            due_date=form.cleaned_data.get("due_date"),
            outcome_note=form.cleaned_data.get("outcome_note"),
        )
        messages.success(request, "Corrective action updated.")
        return HttpResponseRedirect(reverse("mel_feedback:detail", kwargs={"pk": pk}))


class ChannelListView(RoleRequiredMixin, ListView):
    allowed_roles = OFFICER_ROLES
    model = FeedbackChannel
    context_object_name = "channels"
    paginate_by = 10
    template_name = "feedback/channel_list.html"


class ChannelCreateView(RoleRequiredMixin, CreateView):
    """Officer-facing create view — avoids forcing channel setup through Django admin."""

    allowed_roles = OFFICER_ROLES
    model = FeedbackChannel
    form_class = FeedbackChannelForm
    template_name = "feedback/channel_form.html"
    success_url = reverse_lazy("mel_feedback:channel_list")

    def form_valid(self, form):
        form.instance.created_by = self.request.user
        messages.success(self.request, "Feedback channel created.")
        return super().form_valid(form)


class ChannelUpdateView(RoleRequiredMixin, UpdateView):
    allowed_roles = OFFICER_ROLES
    model = FeedbackChannel
    form_class = FeedbackChannelForm
    template_name = "feedback/channel_form.html"
    slug_field = "slug"
    slug_url_kwarg = "slug"
    success_url = reverse_lazy("mel_feedback:channel_list")

    def form_valid(self, form):
        messages.success(self.request, "Feedback channel updated.")
        return super().form_valid(form)


# ---------------------------------------------------------------------------
# Phase 4.3 — Survey builder / take / results
# ---------------------------------------------------------------------------


class SurveyListView(RoleRequiredMixin, ListView):
    allowed_roles = OFFICER_ROLES
    model = Survey
    context_object_name = "surveys"
    paginate_by = 10
    template_name = "feedback/survey_list.html"

    def get_queryset(self):
        from django.db.models import Count, Q

        # Count only COMPLETED responses so the list column matches the results
        # page (in-progress shells are not real responses — D-D11f).
        qs = Survey.objects.all().annotate(
            completed_responses=Count(
                "responses",
                filter=Q(responses__status=SurveyResponse.Status.COMPLETED),
            )
        ).order_by("-created_at")
        status = (self.request.GET.get("status") or "").strip()
        if status:
            qs = qs.filter(status=status)
        return qs

    def get_context_data(self, **kwargs):
        from django.db.models import Count, Q

        ctx = super().get_context_data(**kwargs)
        ctx["status_choices"] = SurveyStatus.choices
        ctx["filters"] = {"status": self.request.GET.get("status", "")}
        ctx["status_stats"] = Survey.objects.aggregate(
            draft=Count("pk", filter=Q(status=SurveyStatus.DRAFT)),
            active=Count("pk", filter=Q(status=SurveyStatus.ACTIVE)),
            closed=Count("pk", filter=Q(status=SurveyStatus.CLOSED)),
        )
        return ctx


class SurveyCreateView(RoleRequiredMixin, View):
    allowed_roles = OFFICER_ROLES
    template_name = "feedback/survey_create.html"

    def get(self, request, slug: str | None = None):
        survey = None
        if slug:
            survey = get_object_or_404(Survey, slug=slug)
        return render(
            request,
            self.template_name,
            {
                "survey": survey,
                "trigger_choices": SurveyTrigger.choices,
                "type_choices": SurveyQuestionType.choices,
            },
        )

    def post(self, request, slug: str | None = None):
        title = (request.POST.get("title") or "").strip()
        description = request.POST.get("description") or ""
        trigger = request.POST.get("trigger") or SurveyTrigger.MANUAL
        response_window_days = int(request.POST.get("response_window_days") or 14)
        is_anonymous = request.POST.get("is_anonymous") == "on"
        # Questions arrive serialised as a JSON blob from the Alpine.js builder.
        try:
            questions = json.loads(request.POST.get("questions_json") or "[]")
        except json.JSONDecodeError:
            questions = []
        if not title:
            messages.error(request, "Survey title is required.")
            return self.get(request, slug=slug)

        if slug:
            survey = get_object_or_404(Survey, slug=slug)
            if survey.status == SurveyStatus.CLOSED:
                messages.error(request, "Closed surveys cannot be edited.")
                return HttpResponseRedirect(reverse("mel_feedback:survey_results", args=[survey.slug]))

            # Metadata (title/description/trigger/window/anonymity) is always
            # safe to edit. Question structure is NOT: deleting + recreating the
            # question set cascades away any SurveyAnswers already collected, so
            # once responses exist we lock the questions (D-D2).
            has_responses = SurveyResponse.objects.filter(survey=survey).exists()

            survey.title = title
            survey.description = description
            survey.trigger = trigger
            survey.response_window_days = response_window_days
            survey.is_anonymous = is_anonymous
            survey.save()

            if has_responses:
                messages.warning(
                    request,
                    "This survey already has collected responses, so its "
                    "questions are locked to protect that data. Only the title, "
                    "description and settings were updated. To change questions, "
                    "close this survey and build a new version.",
                )
                return HttpResponseRedirect(
                    reverse("mel_feedback:survey_results", args=[survey.slug])
                )

            errors = _validate_survey_questions(questions)
            if errors:
                messages.error(request, "Could not save: " + " ".join(errors))
                return self.get(request, slug=slug)

            survey.questions.all().delete()
            for ordinal, q in enumerate(questions):
                SurveyQuestion.objects.create(
                    survey=survey,
                    ordinal=ordinal,
                    page=int(q.get("page") or 1),
                    type=q.get("type") or SurveyQuestionType.SHORT_ANSWER,
                    text=(q.get("text") or "").strip(),
                    help_text=q.get("help_text") or "",
                    required=bool(q.get("required")),
                    options=q.get("options") or [],
                )
        else:
            errors = _validate_survey_questions(questions)
            if errors:
                messages.error(request, "Could not save: " + " ".join(errors))
                return self.get(request, slug=slug)
            survey = create_survey(
                title=title,
                description=description,
                trigger=trigger,
                response_window_days=response_window_days,
                is_anonymous=is_anonymous,
                questions=questions,
                created_by=request.user,
            )
        messages.success(request, "Survey saved.")
        return HttpResponseRedirect(reverse("mel_feedback:survey_results", args=[survey.slug]))


class SurveyActivateView(RoleRequiredMixin, View):
    allowed_roles = OFFICER_ROLES

    def post(self, request, slug: str):
        survey = get_object_or_404(Survey, slug=slug)
        try:
            activate_survey(survey)
        except ValueError as exc:
            messages.error(request, str(exc))
        else:
            messages.success(request, "Survey activated. Backing channel provisioned.")
        return HttpResponseRedirect(reverse("mel_feedback:survey_results", args=[survey.slug]))


class SurveyCloseView(RoleRequiredMixin, View):
    allowed_roles = OFFICER_ROLES

    def post(self, request, slug: str):
        survey = get_object_or_404(Survey, slug=slug)
        close_survey(survey)
        messages.success(request, "Survey closed.")
        return HttpResponseRedirect(reverse("mel_feedback:survey_results", args=[survey.slug]))


class SurveyResultsView(RoleRequiredMixin, View):
    allowed_roles = RESULTS_ROLES
    template_name = "feedback/survey_results.html"

    def get(self, request, slug: str):
        survey = get_object_or_404(Survey, slug=slug)
        _scope_alumni_officer_to_impact(request.user, survey)
        analytics = analyse_responses(survey)
        questions = list(survey.questions.all().order_by("ordinal"))
        # Distinct page count — the KPI reads "N across P pages", where P is the
        # number of distinct pages, not the question count (D-D10).
        page_count = survey.questions.values("page").distinct().count() or 1
        return render(
            request,
            self.template_name,
            {
                "survey": survey,
                "analytics": analytics,
                "questions": questions,
                "page_count": page_count,
            },
        )


class SurveyChartFragmentView(RoleRequiredMixin, View):
    """HTMX endpoint — re-renders the chart for one question."""

    allowed_roles = RESULTS_ROLES

    def get(self, request, slug: str, question_id: int):
        survey = get_object_or_404(Survey, slug=slug)
        _scope_alumni_officer_to_impact(request.user, survey)
        question = get_object_or_404(SurveyQuestion, pk=question_id, survey=survey)
        analytics = analyse_responses(survey)
        return render(
            request,
            "feedback/partials/response_chart.html",
            {
                "survey": survey,
                "question": question,
                "stats": analytics["per_question"].get(question.pk, {}),
            },
        )


class SurveyTakeView(View):
    """Public, token-gated paginated take — no auth required."""

    template_name = "feedback/survey_take.html"

    _CLOSED_MSG = (
        "This survey has closed and is no longer accepting responses. "
        "Thank you for your interest."
    )

    def _resolve(self, dispatch_token: str | None, channel_token: str | None):
        if dispatch_token:
            dispatch = get_object_or_404(SurveyDispatch, token=dispatch_token)
            survey = dispatch.survey
            # A closed survey must refuse dispatch-token takes too (D-D3).
            if survey.status == SurveyStatus.CLOSED or not survey.is_active:
                raise FeedbackWindowClosed(self._CLOSED_MSG, title="Survey closed")
            return survey, dispatch
        if channel_token:
            # Unknown token → 404; known-but-closed → polite page (D-D7).
            channel = FeedbackChannel.objects.filter(token=channel_token).first()
            if channel is None:
                raise Http404
            survey = getattr(channel, "survey", None)
            if survey is None:
                raise Http404
            now = timezone.now()
            if (
                not channel.is_active
                or survey.status == SurveyStatus.CLOSED
                or not survey.is_active
            ):
                raise FeedbackWindowClosed(self._CLOSED_MSG, title="Survey closed")
            if channel.opens_at and now < channel.opens_at:
                raise FeedbackWindowClosed(
                    "This survey has not opened yet. Please check back later.",
                    title="Not open yet",
                )
            if channel.closes_at and now > channel.closes_at:
                raise FeedbackWindowClosed(self._CLOSED_MSG, title="Survey closed")
            return survey, None
        raise Http404

    def get(self, request, dispatch_token: str | None = None, channel_token: str | None = None):
        try:
            survey, dispatch = self._resolve(dispatch_token, channel_token)
        except FeedbackWindowClosed as exc:
            return _render_window_closed(request, exc)
        page = max(1, int(request.GET.get("page") or 1))
        questions = list(survey.questions.filter(page=page).order_by("ordinal"))
        total_pages = (
            survey.questions.order_by("-page").values_list("page", flat=True).first() or 1
        )
        return render(
            request,
            self.template_name,
            {
                "survey": survey,
                "dispatch": dispatch,
                "page": page,
                "total_pages": total_pages,
                "questions": questions,
                "nps_scale": list(range(0, 11)),
            },
        )

    def post(self, request, dispatch_token: str | None = None, channel_token: str | None = None):
        try:
            survey, dispatch = self._resolve(dispatch_token, channel_token)
        except FeedbackWindowClosed as exc:
            return _render_window_closed(request, exc)
        page = max(1, int(request.POST.get("page") or 1))
        total_pages = (
            survey.questions.order_by("-page").values_list("page", flat=True).first() or 1
        )
        page_questions = list(survey.questions.filter(page=page).order_by("ordinal"))
        # Build answers dict from POST keys ``answer_<question_id>``.
        answers: dict[int, str] = {}
        for key, value in request.POST.items():
            if key.startswith("answer_"):
                try:
                    qid = int(key.removeprefix("answer_"))
                except ValueError:
                    continue
                answers[qid] = value

        # Server-side enforcement: every question on this page marked required
        # must have a non-empty answer. Re-render (without saving a partial
        # response) when any are missing so the client can't bypass `required`.
        missing = [
            q for q in page_questions
            if q.required and not str(answers.get(q.pk, "")).strip()
        ]
        if missing:
            # Re-render with what the respondent already typed so a validation
            # failure doesn't wipe their answers (D-D8).
            for q in page_questions:
                q.prefill = answers.get(q.pk, "")
            return render(
                request,
                self.template_name,
                {
                    "survey": survey,
                    "dispatch": dispatch,
                    "page": page,
                    "total_pages": total_pages,
                    "questions": page_questions,
                    "nps_scale": list(range(0, 11)),
                    "error": "Please answer all required questions before continuing.",
                    "answers": answers,
                },
            )

        complete = page >= total_pages
        respondent = request.user if request.user.is_authenticated else None

        # Channel-token takes have no dispatch to key the in-progress response
        # on, so we reuse a single response per (survey, session) across pages —
        # otherwise every page POST spawns a new response and page-1 answers are
        # stranded in an in-progress shell (D-D4).
        session_key = f"mel_survey_resp_{survey.pk}"
        existing_response = None
        if dispatch is None:
            rid = request.session.get(session_key)
            if rid:
                existing_response = SurveyResponse.objects.filter(
                    pk=rid,
                    survey=survey,
                    status=SurveyResponse.Status.IN_PROGRESS,
                ).first()

        response = collect_response(
            survey,
            dispatch=dispatch,
            answers=answers,
            respondent=respondent,
            respondent_email=request.POST.get("email", ""),
            complete=complete,
            response=existing_response,
        )
        if dispatch is None:
            if complete:
                request.session.pop(session_key, None)
            else:
                request.session[session_key] = response.pk
        if complete:
            return render(request, "feedback/survey_take_thanks.html", {"survey": survey})
        next_page = page + 1
        return HttpResponseRedirect(
            request.path + f"?page={next_page}"
        )
