from __future__ import annotations

from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.db.models import Q
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.views.decorators.http import require_POST
from django.views.generic import DetailView, ListView, UpdateView

from apps.alumni.profiles.forms import (
    CertificateFormSet,
    DegreeFormSet,
    EmployerFeedbackRequestForm,
    EmployerFeedbackResponseForm,
    EmploymentFormSet,
    ProfileEditForm,
    PublicationFormSet,
    VisibilityForm,
)
from apps.alumni.profiles.models import (
    AlumniProfile,
    EmployerFeedbackInvitation,
)
from apps.alumni.profiles.services import toggle_visibility
from apps.core.permissions.roles import UserRole


STAFF_ROLES = {
    UserRole.ADMIN.value,
    UserRole.SYSTEM_ADMIN.value,
    UserRole.ALUMNI_OFFICER.value,
    UserRole.MEL_OFFICER.value,
}


def _user_is_staff(user) -> bool:
    if not user.is_authenticated:
        return False
    return (
        user.is_superuser
        or user.is_staff
        or getattr(user, "role", "") in STAFF_ROLES
    )


class DirectoryView(ListView):
    """Public, opt-in alumni directory."""

    model = AlumniProfile
    template_name = "alumni/profiles/directory.html"
    context_object_name = "profiles"

    def get_paginate_by(self, queryset):
        return getattr(settings, "ALUMNI_DIRECTORY_PAGE_SIZE", 24)

    def get_queryset(self):
        from apps.alumni.profiles.search import search_alumni

        mode = (self.request.GET.get("mode") or "fts").strip().lower()
        if mode not in {"fts", "semantic"}:
            mode = "fts"
        return search_alumni(
            query=self.request.GET.get("q") or "",
            country=(self.request.GET.get("country") or "").strip(),
            sector=(self.request.GET.get("sector") or "").strip(),
            discipline=(self.request.GET.get("discipline") or "").strip(),
            viewer=self.request.user,
            mode=mode,
        )

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["q"] = self.request.GET.get("q", "")
        ctx["country"] = self.request.GET.get("country", "")
        ctx["sector"] = self.request.GET.get("sector", "")
        ctx["discipline"] = self.request.GET.get("discipline", "")
        ctx["mode"] = self.request.GET.get("mode", "fts")
        return ctx

    def render_to_response(self, context, **response_kwargs):
        if getattr(self.request, "htmx", False):
            return render(
                self.request,
                "alumni/profiles/partials/directory_results.html",
                context,
            )
        return super().render_to_response(context, **response_kwargs)


class ProfileDetailView(DetailView):
    model = AlumniProfile
    template_name = "alumni/profiles/profile_detail.html"
    context_object_name = "profile"

    def get_queryset(self):
        return AlumniProfile.objects.select_related(
            "user", "user__profile", "current_institution"
        ).prefetch_related(
            "degrees", "employments", "publications__document", "certificates"
        )

    def get_object(self, queryset=None):
        profile = super().get_object(queryset=queryset)
        viewer = self.request.user
        if profile.visibility_consent:
            return profile
        if viewer.is_authenticated and (
            profile.user_id == viewer.pk or _user_is_staff(viewer)
        ):
            return profile
        raise Http404("Profile not available.")

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        viewer = self.request.user
        ctx["can_manage"] = (
            viewer.is_authenticated
            and viewer.pk != self.object.user_id
            and _user_is_staff(viewer)
        )
        return ctx


class ProfileEditView(LoginRequiredMixin, UpdateView):
    model = AlumniProfile
    form_class = ProfileEditForm
    template_name = "alumni/profiles/profile_edit.html"

    def get_object(self, queryset=None):
        pk = self.kwargs.get("pk")
        if pk is None:
            profile, _ = AlumniProfile.objects.get_or_create(user=self.request.user)
            return profile
        profile = get_object_or_404(AlumniProfile, pk=pk)
        if profile.user_id != self.request.user.pk and not _user_is_staff(self.request.user):
            raise Http404("Not your profile.")
        return profile

    def get_context_data(self, **kwargs):
        from apps.alumni.profiles import linkedin as li

        ctx = super().get_context_data(**kwargs)
        profile = self.object
        ctx["degree_formset"] = DegreeFormSet(
            self.request.POST or None, instance=profile, prefix="degrees"
        )
        ctx["employment_formset"] = EmploymentFormSet(
            self.request.POST or None, instance=profile, prefix="employments"
        )
        ctx["publication_formset"] = PublicationFormSet(
            self.request.POST or None, instance=profile, prefix="publications"
        )
        ctx["certificate_formset"] = CertificateFormSet(
            self.request.POST or None,
            self.request.FILES or None,
            instance=profile,
            prefix="certificates",
        )
        ctx["visibility_form"] = VisibilityForm(instance=profile)
        ctx["linkedin_oauth_enabled"] = li.is_enabled()
        ctx["linkedin_sync_log"] = getattr(profile, "linkedin_sync_log", None)
        return ctx

    def form_valid(self, form):
        ctx = self.get_context_data(form=form)
        formsets = [
            ctx["degree_formset"],
            ctx["employment_formset"],
            ctx["publication_formset"],
            ctx["certificate_formset"],
        ]
        if not all(fs.is_valid() for fs in formsets):
            return self.form_invalid(form)
        self.object = form.save()
        for fs in formsets:
            fs.instance = self.object
            fs.save()
        messages.success(self.request, "Profile updated.")
        return redirect("alumni_profiles:profile_edit", pk=self.object.pk)


@login_required
@require_POST
def visibility_toggle_view(request, pk: int) -> HttpResponse:
    profile = get_object_or_404(AlumniProfile, pk=pk)
    if profile.user_id != request.user.pk and not _user_is_staff(request.user):
        raise Http404("Not your profile.")
    consent = request.POST.get("consent") == "on"
    toggle_visibility(profile, consent)
    if getattr(request, "htmx", False):
        return render(
            request,
            "alumni/profiles/partials/visibility_toggle.html",
            {"profile": profile},
        )
    messages.success(
        request,
        "Profile published to the directory." if consent else "Profile hidden from the directory.",
    )
    return redirect("alumni_profiles:profile_edit", pk=profile.pk)


class ProfileAdminListView(LoginRequiredMixin, UserPassesTestMixin, ListView):
    model = AlumniProfile
    template_name = "alumni/profiles/profile_admin_list.html"
    context_object_name = "profiles"
    paginate_by = 40

    def test_func(self):
        return _user_is_staff(self.request.user)

    def get_queryset(self):
        # Self-heal: every alumnus should have a profile so officers can see and
        # manage them here — including alumni bulk-created by an admin who have
        # never logged in to trigger the lazy get_or_create in ProfileEditView.
        # bulk_create(ignore_conflicts) is idempotent and cheap on repeat views.
        from django.contrib.auth import get_user_model

        from apps.core.permissions.roles import UserRole

        User = get_user_model()
        missing = User.objects.filter(role=UserRole.ALUMNI).exclude(
            pk__in=AlumniProfile.objects.values("user_id")
        )
        to_create = [AlumniProfile(user=u) for u in missing]
        if to_create:
            AlumniProfile.objects.bulk_create(to_create, ignore_conflicts=True)
        return AlumniProfile.objects.select_related(
            "user", "current_institution"
        ).order_by("-updated_at")


@login_required
def linkedin_connect_view(request) -> HttpResponse:
    """Kick off the LinkedIn OAuth round-trip for the current user's alumni profile."""
    from apps.alumni.profiles import linkedin as li

    if not li.is_enabled():
        messages.info(request, "LinkedIn sync is not configured on this server.")
        return redirect("alumni_profiles:profile_edit_self")
    try:
        url, state = li.build_authorize_url(request)
    except li.LinkedInConfigError:
        messages.error(request, "LinkedIn sync is not configured on this server.")
        return redirect("alumni_profiles:profile_edit_self")
    request.session["linkedin_oauth_state"] = state
    return redirect(url)


@login_required
def linkedin_callback_view(request) -> HttpResponse:
    """Receive LinkedIn's redirect, exchange code, and update the profile."""
    from apps.alumni.profiles import linkedin as li

    code = request.GET.get("code") or ""
    state = request.GET.get("state") or ""
    expected_state = request.session.pop("linkedin_oauth_state", "")
    profile, _ = AlumniProfile.objects.get_or_create(user=request.user)

    if request.GET.get("error"):
        li.record_sync_attempt(
            profile,
            snapshot=None,
            error=f"linkedin_error: {request.GET.get('error_description') or request.GET['error']}",
        )
        messages.error(request, "LinkedIn declined the sign-in. Nothing was changed.")
        return redirect("alumni_profiles:profile_edit_self")

    if not code or not state or state != expected_state:
        li.record_sync_attempt(profile, snapshot=None, error="missing or mismatched state/code")
        messages.error(request, "LinkedIn callback was invalid. Nothing was changed.")
        return redirect("alumni_profiles:profile_edit_self")

    try:
        token = li.exchange_code_for_token(code, redirect_uri=li._redirect_url(request))
        snapshot = li.fetch_userinfo(token)
    except Exception as exc:  # pragma: no cover - network/path
        li.record_sync_attempt(profile, snapshot=None, error=str(exc))
        messages.error(request, f"LinkedIn sync failed: {exc}")
        return redirect("alumni_profiles:profile_edit_self")

    filled = li.apply_snapshot(profile, snapshot)
    li.record_sync_attempt(profile, snapshot=snapshot, fields_filled=filled)
    if filled:
        messages.success(
            request,
            "Connected LinkedIn. Filled: " + ", ".join(filled) + ". Edit anything that's wrong.",
        )
    else:
        messages.info(
            request,
            "Connected LinkedIn — nothing new to fill since your profile already covers what LinkedIn returned.",
        )
    return redirect("alumni_profiles:profile_edit_self")


@login_required
def employer_feedback_request_view(request) -> HttpResponse:
    """The alumna/us invites an employer to fill the curriculum-feedback survey."""
    from apps.alumni.profiles.services_tracer import launch_employer_feedback

    profile, _ = AlumniProfile.objects.get_or_create(user=request.user)
    if request.method == "POST":
        form = EmployerFeedbackRequestForm(request.POST)
        if form.is_valid():
            launch_employer_feedback(
                profile,
                employer_email=form.cleaned_data["employer_email"],
                employer_name=form.cleaned_data.get("employer_name", ""),
            )
            messages.success(
                request,
                "Invitation sent. Your employer will receive a token link to fill the survey.",
            )
            return redirect("alumni_profiles:profile_edit_self")
    else:
        form = EmployerFeedbackRequestForm()
    return render(
        request,
        "alumni/profiles/employer_feedback_request.html",
        {"form": form, "profile": profile},
    )


def employer_feedback_form_view(request, token: str) -> HttpResponse:
    """Public token-gated survey filled by the employer (no login)."""
    from django.utils import timezone

    from apps.mel.feedback.models import TracerResponse

    invitation = get_object_or_404(EmployerFeedbackInvitation, token=token)
    if invitation.responded_at is not None:
        return render(
            request,
            "alumni/profiles/employer_feedback_done.html",
            {"invitation": invitation, "already": True},
        )
    if request.method == "POST":
        form = EmployerFeedbackResponseForm(request.POST)
        if form.is_valid():
            payload = {k: v for k, v in form.cleaned_data.items()}
            TracerResponse.objects.create(
                study=invitation.study,
                respondent_email=invitation.employer_email,
                respondent_name=invitation.employer_name,
                role=form.cleaned_data.get("role_title", ""),
                employment_status=form.cleaned_data.get("employment_status", ""),
                payload=payload,
            )
            invitation.responded_at = timezone.now()
            invitation.save(update_fields=["responded_at"])
            return render(
                request,
                "alumni/profiles/employer_feedback_done.html",
                {"invitation": invitation, "already": False},
            )
    else:
        form = EmployerFeedbackResponseForm()
    return render(
        request,
        "alumni/profiles/employer_feedback_form.html",
        {"form": form, "invitation": invitation},
    )


class EmployerFeedbackReportView(LoginRequiredMixin, UserPassesTestMixin, ListView):
    """Officer-facing report aggregating employer feedback responses."""

    template_name = "alumni/profiles/employer_feedback_report.html"
    context_object_name = "invitations"
    paginate_by = 50

    def test_func(self):
        return _user_is_staff(self.request.user)

    def get_queryset(self):
        return (
            EmployerFeedbackInvitation.objects.select_related(
                "profile", "profile__user", "study"
            )
            .order_by("-invited_at")
        )

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

        from apps.mel.feedback.models import TracerResponse

        ctx = super().get_context_data(**kwargs)
        responses = TracerResponse.objects.filter(
            study__target_population="Employers — programme curriculum feedback"
        )
        ctx["response_count"] = responses.count()
        # Aggregate "preparedness" rating across response payloads.
        ratings = []
        for r in responses.only("payload"):
            try:
                v = int((r.payload or {}).get("preparedness", 0))
                if 1 <= v <= 5:
                    ratings.append(v)
            except (TypeError, ValueError):
                continue
        ctx["avg_rating"] = round(sum(ratings) / len(ratings), 2) if ratings else None
        ctx["rating_n"] = len(ratings)
        ctx["invitation_total"] = EmployerFeedbackInvitation.objects.count()
        ctx["invitation_responded"] = EmployerFeedbackInvitation.objects.filter(
            responded_at__isnull=False
        ).count()
        return ctx


@login_required
def publication_document_search(request) -> HttpResponse:
    """HTMX autocomplete over repository documents for publication linking.

    ``prefix`` identifies the publication formset row that fired the search so
    each result's "Link" action can target that row's hidden ``document`` input.
    """
    q = (request.GET.get("q") or "").strip()
    prefix = (request.GET.get("prefix") or "").strip()
    results = []
    if q:
        try:
            from apps.repository.documents.models import Document  # noqa: PLC0415

            results = list(
                Document.objects.filter(Q(title__icontains=q) | Q(doi__icontains=q))[:10]
            )
        except Exception:  # pragma: no cover - repository optional in tests
            results = []
    return render(
        request,
        "alumni/profiles/partials/document_search_results.html",
        {"results": results, "prefix": prefix, "q": q},
    )


class AlumniInsightsDashboardView(LoginRequiredMixin, UserPassesTestMixin, ListView):
    """Analytics & KPI dashboard for alumni data — staff/admin only."""

    model = AlumniProfile
    template_name = "alumni/profiles/insights.html"
    context_object_name = "profiles"
    paginate_by = 25

    def test_func(self):
        return _user_is_staff(self.request.user)

    def get_queryset(self):
        return AlumniProfile.objects.filter(
            visibility_consent=True
        ).select_related("user", "current_institution").prefetch_related("degrees")

    def get_context_data(self, **kwargs):
        from apps.alumni.profiles.models import Degree
        from django.db.models import Count
        from django.db.models.functions import ExtractYear

        ctx = super().get_context_data(**kwargs)
        qs = AlumniProfile.objects.filter(visibility_consent=True)
        total = qs.count()

        ctx["total"] = total
        ctx["with_institution"] = qs.filter(current_institution__isnull=False).count()
        ctx["with_degree"] = Degree.objects.filter(profile__in=qs).values("profile").distinct().count()
        ctx["mentor_accepting"] = qs.filter(mentor_accepting=True).count()
        ctx["with_photo"] = qs.exclude(photo="").exclude(photo__isnull=True).count()

        # Gender
        from apps.core.authentication.models import UserProfile as UP
        gender_qs = (
            UP.objects.filter(user__alumni_profile__isnull=False)
            .exclude(gender="")
            .values("gender")
            .annotate(n=Count("id"))
            .order_by("-n")
        )
        ctx["gender_data"] = list(gender_qs)
        ctx["gender_total"] = sum(r["n"] for r in ctx["gender_data"])

        # Institution distribution
        ctx["institution_data"] = list(
            qs.filter(current_institution__isnull=False)
            .values("current_institution__name")
            .annotate(n=Count("id"))
            .order_by("-n")[:15]
        )

        # Country distribution
        country_qs = (
            qs.filter(current_institution__isnull=False)
            .exclude(current_institution__country="")
            .exclude(current_institution__country__isnull=True)
            .values("current_institution__country")
            .annotate(n=Count("id"))
            .order_by("-n")
        )
        ctx["country_data"] = [
            {"country": r["current_institution__country"], "n": r["n"]}
            for r in country_qs
        ][:20]

        # Graduation year trend
        year_qs = (
            Degree.objects.filter(profile__in=qs)
            .exclude(graduated_on__year__lt=1990)
            .exclude(graduated_on__year__gt=2027)
            .annotate(year=ExtractYear("graduated_on"))
            .values("year")
            .annotate(n=Count("id"))
            .order_by("year")
        )
        ctx["graduation_trend"] = list(year_qs)
        ctx["graduation_trend_max"] = max((r["n"] for r in ctx["graduation_trend"]), default=1)

        # Degree types
        ctx["degree_type_data"] = list(
            Degree.objects.filter(profile__in=qs)
            .values("degree_type")
            .annotate(n=Count("id"))
            .order_by("-n")
        )

        # Recent alumni
        ctx["recent_alumni"] = list(
            qs.select_related("user", "current_institution")
            .order_by("-published_at", "-id")[:8]
        )

        return ctx
