from __future__ import annotations

from datetime import timedelta

from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.core.exceptions import ValidationError
from django.http import Http404, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils import timezone
from django.views.decorators.http import require_POST
from django.views.generic import CreateView, DetailView, ListView, UpdateView

from apps.alumni.engagement.forms import (
    AlumniEventForm,
    MentorshipRequestForm,
    NetworkGroupForm,
)
from apps.alumni.engagement.models import (
    AlumniEvent,
    ChapterLeadershipRequest,
    EventRegistration,
    EventStatus,
    EventVisibility,
    GroupMembership,
    LeadershipRequestStatus,
    MentorshipPairing,
    MentorshipStatus,
    NetworkGroup,
)
from apps.alumni.engagement.services import (
    announce_event_to_groups,
    join_group,
    leave_group,
    notify_officers_of_event_submission,
    notify_officers_of_leadership_request,
    notify_organiser_of_event_decision,
    notify_requester_of_leadership_decision,
    post_group_announcement,
    recommend_mentors,
    register_for_event,
    remove_member,
    request_mentorship,
    respond_to_mentorship,
    set_member_role,
)
from apps.alumni.profiles.models import AlumniProfile
from apps.alumni.profiles.views import _user_is_staff


# ---------------------------------------------------------------------------
# Events
# ---------------------------------------------------------------------------


def visible_events_for(viewer):
    """Queryset of events ``viewer`` is allowed to see.

    The single source of truth for event visibility, shared by the list, the
    month-grid calendar, the detail view and the per-event .ics download so all
    four honour the same gates:

    * public          → everyone (including anonymous)
    * alumni_only     → any authenticated (known) user
    * group           → members of one of the event's groups (or staff)

    Only APPROVED events reach non-staff viewers. An organiser always sees
    their own event (even while it is PENDING or REJECTED) so they can track
    its moderation state; staff see everything. Using the same queryset
    everywhere means an unauthorized viewer simply can't resolve the object
    (404), never a 403 that would leak the event's existence.
    """
    from django.db.models import Q

    qs = AlumniEvent.objects.prefetch_related("groups")
    if not viewer.is_authenticated:
        return qs.filter(
            visibility=EventVisibility.PUBLIC, status=EventStatus.APPROVED
        )
    if _user_is_staff(viewer):
        return qs
    group_ids = list(
        GroupMembership.objects.filter(user=viewer).values_list("group_id", flat=True)
    )
    audience = (
        Q(visibility=EventVisibility.PUBLIC)
        | Q(visibility=EventVisibility.ALUMNI_ONLY)
        | Q(visibility=EventVisibility.GROUP, groups__in=group_ids)
    )
    return qs.filter(
        (Q(status=EventStatus.APPROVED) & audience) | Q(organiser=viewer)
    ).distinct()


class EventListView(ListView):
    model = AlumniEvent
    template_name = "alumni/engagement/event_list.html"
    context_object_name = "events"
    paginate_by = 25

    def _visible_events(self):
        """Events the current viewer may see, before ordering / pagination.

        Shared by the paginated list and the month-grid calendar so both honour
        the same visibility gates (public / alumni-only / group-restricted).
        """
        return visible_events_for(self.request.user)

    def get_queryset(self):
        from datetime import datetime

        qs = self._visible_events()
        # A single-day drill-down used by the calendar's day links / "+N more".
        day = self.request.GET.get("day")
        if day:
            try:
                d = datetime.strptime(day, "%Y-%m-%d").date()
                # Overlap match so multi-day events surface on every covered day.
                qs = qs.filter(start_at__date__lte=d, end_at__date__gte=d)
            except ValueError:
                pass
        return qs.order_by("start_at" if day else "-start_at")

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        view_mode = self.request.GET.get("view", "list")
        ctx["view_mode"] = "calendar" if view_mode == "calendar" else "list"
        ctx["filter_day"] = self._parse_day()
        if ctx["view_mode"] == "calendar":
            ctx.update(self._calendar_context())
        return ctx

    def _parse_day(self):
        from datetime import datetime

        raw = self.request.GET.get("day")
        if not raw:
            return None
        try:
            return datetime.strptime(raw, "%Y-%m-%d").date()
        except ValueError:
            return None

    # Cap on pills rendered per day cell before collapsing into "+N more".
    CALENDAR_MAX_PER_DAY = 3

    def _calendar_context(self):
        """Build a Monday-first month grid of the viewer's visible events.

        Events are multi-day aware (rendered on every day they span, clamped to
        the visible grid) and carry per-viewer RSVP + past/upcoming flags.
        """
        import calendar as _calendar
        from datetime import date

        from django.utils import timezone

        today = timezone.localdate()
        now = timezone.now()
        try:
            year = int(self.request.GET.get("year") or today.year)
            month = int(self.request.GET.get("month") or today.month)
            base = date(year, month, 1)
        except (ValueError, TypeError):
            base = today.replace(day=1)

        if base.month == 12:
            next_month = date(base.year + 1, 1, 1)
        else:
            next_month = date(base.year, base.month + 1, 1)
        prev_month = (base - timedelta(days=1)).replace(day=1)

        cal = _calendar.Calendar(firstweekday=0)  # Monday-first
        weeks_dates = cal.monthdatescalendar(base.year, base.month)
        grid_start = weeks_dates[0][0]
        grid_end = weeks_dates[-1][-1]

        # Events overlapping the *visible grid* (not just the month) so leading /
        # trailing days and multi-day spans render correctly.
        grid_events = list(
            self._visible_events()
            .filter(start_at__date__lte=grid_end, end_at__date__gte=grid_start)
            .order_by("start_at")
        )

        registered_ids: set[int] = set()
        viewer = self.request.user
        if viewer.is_authenticated and grid_events:
            registered_ids = set(
                EventRegistration.objects.filter(
                    user=viewer, event__in=grid_events
                ).values_list("event_id", flat=True)
            )

        # Bucket each event onto every date it covers within the grid.
        events_by_date: dict[date, list] = {}
        for event in grid_events:
            s = timezone.localtime(event.start_at).date()
            e = timezone.localtime(event.end_at).date()
            if e < s:
                e = s
            span = max(s, grid_start)
            span_end = min(e, grid_end)
            while span <= span_end:
                events_by_date.setdefault(span, []).append(
                    {
                        "event": event,
                        "is_start": span == s,
                        "is_registered": event.pk in registered_ids,
                        "is_past": event.end_at < now,
                    }
                )
                span += timedelta(days=1)

        max_per_day = self.CALENDAR_MAX_PER_DAY
        month_event_ids: set[int] = set()
        weeks = []
        for week in weeks_dates:
            cells = []
            for cell_date in week:
                in_month = cell_date.month == base.month
                day_events = events_by_date.get(cell_date, [])
                if in_month:
                    for de in day_events:
                        month_event_ids.add(de["event"].pk)
                cells.append(
                    {
                        "date": cell_date,
                        "in_month": in_month,
                        "is_today": cell_date == today,
                        "is_weekend": cell_date.weekday() >= 5,
                        "events": day_events[:max_per_day],
                        "overflow": max(0, len(day_events) - max_per_day),
                        "event_total": len(day_events),
                    }
                )
            weeks.append(cells)

        return {
            "calendar_weeks": weeks,
            "calendar_month": base,
            "calendar_prev": prev_month,
            "calendar_next": next_month,
            "calendar_today": today,
            "calendar_weekdays": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
            "calendar_event_count": len(month_event_ids),
            "calendar_month_num": base.month,
            "calendar_year_num": base.year,
            "calendar_month_choices": [
                (i, date(2000, i, 1).strftime("%B")) for i in range(1, 13)
            ],
            "calendar_year_choices": list(range(today.year - 3, today.year + 4)),
        }


class EventDetailView(DetailView):
    model = AlumniEvent
    template_name = "alumni/engagement/event_detail.html"
    context_object_name = "event"
    slug_field = "slug"
    slug_url_kwarg = "slug"

    def get_queryset(self):
        # Gate the object lookup by visibility so restricted events (and their
        # private virtual_url) can't be read by an unauthorised slug guess.
        # An unauthorised viewer gets a 404, never a 403 that leaks existence.
        return visible_events_for(self.request.user)

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

        from apps.alumni.profiles.models import AlumniProfile

        ctx = super().get_context_data(**kwargs)
        event = self.object
        viewer = self.request.user
        now = timezone.now()

        my_registration = None
        if viewer.is_authenticated:
            my_registration = EventRegistration.objects.filter(
                user=viewer, event=event
            ).first()
        ctx["my_registration"] = my_registration
        ctx["is_registered"] = my_registration is not None
        ctx["is_past"] = event.end_at < now

        # Post-event feedback capture: registered attendee, event has ended.
        ctx["can_leave_feedback"] = bool(ctx["is_past"] and my_registration)
        ctx["has_left_feedback"] = bool(
            my_registration and my_registration.feedback_rating
        )

        # Organiser / staff attendance-marking affordance.
        ctx["can_manage_attendance"] = viewer.is_authenticated and (
            _user_is_staff(viewer) or event.organiser_id == viewer.pk
        )

        # Moderation affordances: staff can approve/reject a pending event; the
        # organiser sees their own event's moderation state.
        ctx["can_moderate"] = _user_is_staff(viewer)
        ctx["is_pending"] = event.status == EventStatus.PENDING
        ctx["is_rejected"] = event.status == EventStatus.REJECTED
        ctx["is_organiser"] = (
            viewer.is_authenticated and event.organiser_id == viewer.pk
        )

        regs = EventRegistration.objects.filter(event=event).select_related(
            "user", "user__alumni_profile"
        )
        ctx["registration_count"] = regs.count()
        if event.capacity:
            pct = int(min(100, (ctx["registration_count"] / event.capacity) * 100))
            ctx["capacity_pct"] = pct
            ctx["seats_left"] = max(0, event.capacity - ctx["registration_count"])
        else:
            ctx["capacity_pct"] = None
            ctx["seats_left"] = None

        # Consented alumni who RSVP'd — for the "who's coming" preview.
        registrant_user_ids = list(regs.values_list("user_id", flat=True))
        ctx["registrant_profiles"] = list(
            AlumniProfile.objects.filter(
                user_id__in=registrant_user_ids, visibility_consent=True
            )
            .select_related("user")
            .order_by("-published_at")[:10]
        )

        # Organiser — try to surface their alumni profile for a nicer card.
        ctx["organiser_profile"] = None
        if event.organiser_id:
            ctx["organiser_profile"] = AlumniProfile.objects.filter(
                user_id=event.organiser_id
            ).select_related("user").first()

        # Feedback summary for past events.
        if ctx["is_past"]:
            attended_qs = regs.filter(attended=True)
            rated_qs = regs.exclude(feedback_rating__isnull=True)
            ctx["attended_count"] = attended_qs.count()
            agg = rated_qs.aggregate(avg=Avg("feedback_rating"), n=Count("pk"))
            ctx["feedback_avg"] = round(agg["avg"], 2) if agg["avg"] else None
            ctx["feedback_count"] = agg["n"] or 0

        # Related upcoming events.
        related_qs = (
            AlumniEvent.objects.filter(end_at__gte=now)
            .exclude(pk=event.pk)
            .order_by("start_at")
        )
        if event.visibility == EventVisibility.GROUP and event.groups.exists():
            related_qs = related_qs.filter(groups__in=event.groups.all()).distinct()
        ctx["related_events"] = list(related_qs[:4])

        ctx["event_groups"] = list(event.groups.all()[:8])
        return ctx


def _user_moderates_group(user, group: NetworkGroup) -> bool:
    if not user.is_authenticated:
        return False
    if _user_is_staff(user):
        return True
    return GroupMembership.objects.filter(
        user=user, group=group, role="moderator"
    ).exists()


class EventCreateView(LoginRequiredMixin, CreateView):
    """Any authenticated alumnus can propose an event.

    Events proposed by ordinary alumni are held as PENDING and only reach the
    calendar once an alumni officer approves them. Staff (and moderators of a
    group the event is scoped to) are trusted approvers, so their events are
    auto-approved on creation.
    """

    model = AlumniEvent
    form_class = AlumniEventForm
    template_name = "alumni/engagement/event_create.html"

    def _is_auto_approver(self, form) -> bool:
        user = self.request.user
        if _user_is_staff(user):
            return True
        # A moderator of any group the event is scoped to may publish directly.
        groups = form.cleaned_data.get("groups") or []
        return any(_user_moderates_group(user, g) for g in groups)

    def get_initial(self):
        initial = super().get_initial()
        slug = self.request.GET.get("group")
        if slug:
            group = NetworkGroup.objects.filter(slug=slug).first()
            if group:
                initial["visibility"] = EventVisibility.GROUP
                initial["groups"] = [group.pk]
        return initial

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        slug = self.request.GET.get("group")
        ctx["prefill_group"] = (
            NetworkGroup.objects.filter(slug=slug).first() if slug else None
        )
        return ctx

    def form_valid(self, form):
        form.instance.organiser = self.request.user
        auto_approve = self._is_auto_approver(form)
        if auto_approve:
            form.instance.status = EventStatus.APPROVED
            form.instance.reviewed_by = self.request.user
            form.instance.reviewed_at = timezone.now()
        else:
            form.instance.status = EventStatus.PENDING
        # ModelForm.save() with the default commit=True already persists the
        # instance AND the m2m ``groups`` field, so a manual save_m2m() call
        # here raises AttributeError (save_m2m is only attached when
        # commit=False) — which 500'd after the row was already created.
        self.object = form.save()
        if auto_approve:
            # Announce to group members immediately when scoped to groups.
            try:
                announce_event_to_groups(self.object)
            except Exception:
                pass
            messages.success(self.request, "Event created and published to the calendar.")
        else:
            notify_officers_of_event_submission(self.object)
            messages.success(
                self.request,
                "Event submitted for approval — an alumni officer will review it "
                "before it appears on the calendar.",
            )
        return redirect("alumni_engagement:event_detail", slug=self.object.slug)


class EventUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    """Edit an existing event — staff, organiser, or a moderator of one of its groups."""

    model = AlumniEvent
    form_class = AlumniEventForm
    template_name = "alumni/engagement/event_create.html"
    slug_field = "slug"
    slug_url_kwarg = "slug"

    def test_func(self):
        event = self.get_object()
        user = self.request.user
        if _user_is_staff(user):
            return True
        if event.organiser_id == user.pk:
            return True
        return any(_user_moderates_group(user, g) for g in event.groups.all())

    def form_valid(self, form):
        editor = self.request.user
        # A non-staff organiser editing a pending/rejected event re-submits it
        # for review (staff edits leave the moderation state untouched).
        resubmit = (
            not _user_is_staff(editor)
            and form.instance.status in (EventStatus.PENDING, EventStatus.REJECTED)
        )
        if resubmit:
            form.instance.status = EventStatus.PENDING
            form.instance.review_notes = ""
        # commit=True already saves the m2m ``groups``; a manual save_m2m()
        # call raises AttributeError (see EventCreateView.form_valid).
        self.object = form.save()
        if resubmit:
            notify_officers_of_event_submission(self.object)
            messages.success(
                self.request,
                "Event updated and re-submitted for approval.",
            )
        else:
            try:
                announce_event_to_groups(self.object)
            except Exception:
                pass
            messages.success(self.request, "Event updated.")
        return redirect("alumni_engagement:event_detail", slug=self.object.slug)


# ---------------------------------------------------------------------------
# Event moderation (alumni officers / admins)
# ---------------------------------------------------------------------------


class EventApprovalQueueView(LoginRequiredMixin, UserPassesTestMixin, ListView):
    """Alumni officers review events proposed by alumni before they go live."""

    template_name = "alumni/engagement/event_approval_queue.html"
    context_object_name = "events"
    paginate_by = 25

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

    def get_queryset(self):
        return (
            AlumniEvent.objects.filter(status=EventStatus.PENDING)
            .select_related("organiser")
            .prefetch_related("groups")
            .order_by("start_at")
        )


@login_required
@require_POST
def event_approve_view(request, slug: str) -> HttpResponse:
    if not _user_is_staff(request.user):
        raise Http404()
    event = get_object_or_404(AlumniEvent, slug=slug)
    if event.status != EventStatus.APPROVED:
        event.status = EventStatus.APPROVED
        event.reviewed_by = request.user
        event.reviewed_at = timezone.now()
        event.save(update_fields=["status", "reviewed_by", "reviewed_at"])
        try:
            announce_event_to_groups(event)
        except Exception:  # pragma: no cover - defensive
            pass
        notify_organiser_of_event_decision(event, approved=True)
    messages.success(request, f"“{event.title}” approved and published to the calendar.")
    return redirect("alumni_engagement:event_approval_queue")


@login_required
@require_POST
def event_reject_view(request, slug: str) -> HttpResponse:
    if not _user_is_staff(request.user):
        raise Http404()
    event = get_object_or_404(AlumniEvent, slug=slug)
    event.status = EventStatus.REJECTED
    event.reviewed_by = request.user
    event.reviewed_at = timezone.now()
    event.review_notes = (request.POST.get("review_notes") or "").strip()
    event.save(update_fields=["status", "reviewed_by", "reviewed_at", "review_notes"])
    notify_organiser_of_event_decision(event, approved=False)
    messages.success(request, f"“{event.title}” was rejected and the organiser notified.")
    return redirect("alumni_engagement:event_approval_queue")


def event_ics(request, slug: str) -> HttpResponse:
    """Download an .ics calendar file for an event (RFC 5545).

    Gated by the same visibility rule as the detail page — the .ics embeds the
    private virtual_url in LOCATION, so a restricted event must 404 for anyone
    who can't already see it.
    """
    event = get_object_or_404(visible_events_for(request.user), slug=slug)
    fmt = "%Y%m%dT%H%M%SZ"
    dtstart = event.start_at.strftime(fmt)
    dtend = event.end_at.strftime(fmt)
    dtstamp = dtstart

    def esc(text: str) -> str:
        return (
            (text or "")
            .replace("\\", "\\\\")
            .replace("\n", "\\n")
            .replace(",", "\\,")
            .replace(";", "\\;")
        )

    description = esc((event.description or "").strip())
    location = esc(event.location)
    if event.virtual_url:
        location = (location + " · " + event.virtual_url).strip(" ·")

    ics = (
        "BEGIN:VCALENDAR\r\n"
        "VERSION:2.0\r\n"
        "PRODID:-//RUFORUM IILMP//Alumni Events//EN\r\n"
        "CALSCALE:GREGORIAN\r\n"
        "METHOD:PUBLISH\r\n"
        "BEGIN:VEVENT\r\n"
        f"UID:alumni-event-{event.pk}@ruforum.org\r\n"
        f"DTSTAMP:{dtstamp}\r\n"
        f"DTSTART:{dtstart}\r\n"
        f"DTEND:{dtend}\r\n"
        f"SUMMARY:{esc(event.title)}\r\n"
        f"DESCRIPTION:{description}\r\n"
        f"LOCATION:{location}\r\n"
        "END:VEVENT\r\n"
        "END:VCALENDAR\r\n"
    )
    resp = HttpResponse(ics, content_type="text/calendar; charset=utf-8")
    resp["Content-Disposition"] = f'attachment; filename="{event.slug}.ics"'
    return resp


def _ics_escape(text: str) -> str:
    return (
        (text or "")
        .replace("\\", "\\\\")
        .replace("\n", "\\n")
        .replace(",", "\\,")
        .replace(";", "\\;")
    )


def events_ics_feed(request) -> HttpResponse:
    """Subscribable calendar feed of upcoming **public** alumni events.

    Calendar clients poll this URL without session cookies, so it can only
    safely expose PUBLIC events — alumni-only / group-restricted events are
    never included regardless of who fetches it.
    """
    from django.utils import timezone

    now = timezone.now()
    events = (
        AlumniEvent.objects.filter(
            visibility=EventVisibility.PUBLIC, end_at__gte=now
        )
        .order_by("start_at")[:200]
    )
    fmt = "%Y%m%dT%H%M%SZ"
    lines = [
        "BEGIN:VCALENDAR",
        "VERSION:2.0",
        "PRODID:-//RUFORUM IILMP//Alumni Events//EN",
        "CALSCALE:GREGORIAN",
        "METHOD:PUBLISH",
        "X-WR-CALNAME:RUFORUM Alumni Events",
        "X-WR-CALDESC:Upcoming public alumni events from the RUFORUM network",
    ]
    for event in events:
        location = _ics_escape(event.location)
        if event.virtual_url:
            location = (location + " · " + event.virtual_url).strip(" ·")
        url = request.build_absolute_uri(
            reverse("alumni_engagement:event_detail", args=[event.slug])
        )
        lines += [
            "BEGIN:VEVENT",
            f"UID:alumni-event-{event.pk}@ruforum.org",
            f"DTSTAMP:{event.start_at.strftime(fmt)}",
            f"DTSTART:{event.start_at.strftime(fmt)}",
            f"DTEND:{event.end_at.strftime(fmt)}",
            f"SUMMARY:{_ics_escape(event.title)}",
            f"DESCRIPTION:{_ics_escape((event.description or '').strip())}",
            f"LOCATION:{location}",
            f"URL:{url}",
            "END:VEVENT",
        ]
    lines.append("END:VCALENDAR")
    ics = "\r\n".join(lines) + "\r\n"
    resp = HttpResponse(ics, content_type="text/calendar; charset=utf-8")
    resp["Content-Disposition"] = 'inline; filename="ruforum-alumni-events.ics"'
    return resp


@login_required
@require_POST
def event_rsvp(request, slug: str) -> HttpResponse:
    event = get_object_or_404(AlumniEvent, slug=slug)
    try:
        reg = register_for_event(request.user, event)
    except ValidationError as exc:
        messages.error(request, str(exc))
        return redirect("alumni_engagement:event_detail", slug=event.slug)
    if getattr(request, "htmx", False):
        return render(
            request,
            "alumni/engagement/partials/registration_status.html",
            {"event": event, "is_registered": True},
        )
    messages.success(request, "You are registered.")
    return redirect("alumni_engagement:event_detail", slug=event.slug)


@login_required
@require_POST
def event_feedback(request, slug: str) -> HttpResponse:
    """Capture a registered attendee's post-event rating + optional comment.

    Only available once the event has ended and only to a user who RSVP'd.
    Submitting implies attendance. Re-submitting updates the same row so a
    user can revise their rating without creating duplicates.
    """
    from apps.alumni.engagement.services import record_event_feedback

    event = get_object_or_404(visible_events_for(request.user), slug=slug)
    try:
        rating = int(request.POST.get("rating") or 0)
    except (TypeError, ValueError):
        rating = 0
    try:
        record_event_feedback(
            request.user,
            event,
            rating=rating,
            text=(request.POST.get("feedback_text") or "").strip(),
        )
        messages.success(request, "Thanks, your feedback is recorded.")
    except ValidationError as exc:
        messages.error(request, str(exc))
    return redirect("alumni_engagement:event_detail", slug=event.slug)


@login_required
def event_attendance(request, slug: str) -> HttpResponse:
    """Organiser / staff roster to mark which registrants attended.

    A single bulk form: every checked registrant is set attended, unchecked
    ones are cleared, in one POST.
    """
    event = get_object_or_404(AlumniEvent, slug=slug)
    if not (_user_is_staff(request.user) or event.organiser_id == request.user.pk):
        raise Http404("You do not manage this event.")

    regs = (
        EventRegistration.objects.filter(event=event)
        .select_related("user", "user__alumni_profile")
        .order_by("-attended", "registered_at")
    )

    if request.method == "POST":
        checked = set(request.POST.getlist("attended"))
        updated = 0
        for reg in regs:
            should = str(reg.pk) in checked
            if reg.attended != should:
                reg.attended = should
                reg.save(update_fields=["attended"])
                updated += 1
        messages.success(
            request,
            f"Attendance updated for {updated} registrant{'' if updated == 1 else 's'}."
            if updated
            else "No changes to attendance.",
        )
        return redirect("alumni_engagement:event_attendance", slug=event.slug)

    return render(
        request,
        "alumni/engagement/event_attendance.html",
        {
            "event": event,
            "registrations": list(regs),
            "attended_count": sum(1 for r in regs if r.attended),
        },
    )


# ---------------------------------------------------------------------------
# Mentorship
# ---------------------------------------------------------------------------


class MyMentorshipsView(LoginRequiredMixin, ListView):
    template_name = "alumni/engagement/mentorship_list.html"
    context_object_name = "pairings"
    paginate_by = 50

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

        viewer = self.request.user
        qs = (
            MentorshipPairing.objects.filter(
                Q(mentor__user=viewer) | Q(mentee=viewer)
            )
            .select_related("mentor", "mentor__user", "mentee", "mentee__alumni_profile")
            .annotate(session_count=Count("session_notes"))
            .order_by("-created_at")
        )
        role = self.request.GET.get("role")
        if role == "mentee":
            qs = qs.filter(mentee=viewer)
        elif role == "mentor":
            qs = qs.filter(mentor__user=viewer)
        status = self.request.GET.get("status")
        if status == "active":
            qs = qs.filter(status=MentorshipStatus.ACTIVE)
        elif status == "requested":
            qs = qs.filter(status=MentorshipStatus.REQUESTED)
        elif status == "past":
            qs = qs.filter(
                status__in=[
                    MentorshipStatus.COMPLETED,
                    MentorshipStatus.DECLINED,
                    MentorshipStatus.CANCELLED,
                ]
            )
        return qs

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

        ctx = super().get_context_data(**kwargs)
        viewer = self.request.user

        # Unfiltered base for stat cards and grouping.
        base = MentorshipPairing.objects.filter(
            Q(mentor__user=viewer) | Q(mentee=viewer)
        )

        ctx["role"] = self.request.GET.get("role") or "all"
        ctx["status_filter"] = self.request.GET.get("status") or "all"

        ctx["stats"] = {
            "action_needed": base.filter(
                status=MentorshipStatus.REQUESTED, mentor__user=viewer
            ).count(),
            "pending_response": base.filter(
                status=MentorshipStatus.REQUESTED, mentee=viewer
            ).count(),
            "active_as_mentee": base.filter(
                status=MentorshipStatus.ACTIVE, mentee=viewer
            ).count(),
            "active_as_mentor": base.filter(
                status=MentorshipStatus.ACTIVE, mentor__user=viewer
            ).count(),
            "completed": base.filter(status=MentorshipStatus.COMPLETED).count(),
        }

        # Grouped lists for the 3-panel layout.
        paired = (
            base.select_related("mentor", "mentor__user", "mentee", "mentee__alumni_profile")
            .annotate(session_count=Count("session_notes"))
        )
        ctx["action_needed"] = list(
            paired.filter(status=MentorshipStatus.REQUESTED, mentor__user=viewer)
            .order_by("-created_at")
        )
        ctx["pending_from_me"] = list(
            paired.filter(status=MentorshipStatus.REQUESTED, mentee=viewer)
            .order_by("-created_at")
        )
        ctx["active_list"] = list(
            paired.filter(status=MentorshipStatus.ACTIVE).order_by("-started_at")
        )
        ctx["past_list"] = list(
            paired.filter(
                status__in=[
                    MentorshipStatus.COMPLETED,
                    MentorshipStatus.DECLINED,
                    MentorshipStatus.CANCELLED,
                ]
            ).order_by("-ended_at", "-created_at")[:20]
        )

        # Surface the viewer's own mentor-accepting state for the "offer to mentor" CTA.
        alumni_profile = AlumniProfile.objects.filter(user=viewer).first()
        ctx["viewer_profile"] = alumni_profile
        ctx["viewer_mentor_accepting"] = bool(
            alumni_profile and alumni_profile.mentor_accepting
        )
        return ctx


class MentorshipDiscoverView(LoginRequiredMixin, ListView):
    template_name = "alumni/engagement/mentorship_discover.html"
    context_object_name = "recommendations"

    def get_queryset(self):
        topic = (self.request.GET.get("topic") or "").strip() or None
        country = (self.request.GET.get("country") or "").strip() or None
        tag = (self.request.GET.get("tag") or "").strip()
        include_full = self.request.GET.get("all") == "1"
        extra_tags = [tag] if tag else []
        return recommend_mentors(
            self.request.user,
            topic=topic,
            country_override=country,
            extra_tags=extra_tags,
            include_unavailable=include_full,
            limit=24,
        )

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["topic"] = self.request.GET.get("topic", "")
        ctx["country"] = self.request.GET.get("country", "")
        ctx["tag"] = self.request.GET.get("tag", "")
        ctx["include_unavailable"] = self.request.GET.get("all") == "1"
        # Topics that accepting mentors have actually declared they mentor on —
        # drives the "what do you want help with?" dropdown so mentees pick a
        # real topic the matcher can score against, instead of guessing free text.
        topics: set[str] = set()
        for row in (
            AlumniProfile.objects.filter(mentor_accepting=True)
            .exclude(mentor_topics=[])
            .values_list("mentor_topics", flat=True)
        ):
            for t in row or []:
                cleaned = (t or "").strip()
                if cleaned:
                    topics.add(cleaned)
        ctx["mentor_topic_choices"] = sorted(topics, key=str.lower)
        return ctx


@login_required
def mentorship_request_view(request, pk: int) -> HttpResponse:
    mentor_profile = get_object_or_404(AlumniProfile, pk=pk)
    if mentor_profile.user_id == request.user.pk:
        messages.error(request, "Cannot mentor yourself.")
        return redirect("alumni_profiles:profile_detail", pk=pk)
    if not mentor_profile.mentor_accepting:
        messages.info(
            request,
            f"{mentor_profile.display_name} isn't accepting new mentees right now.",
        )
        return redirect("alumni_profiles:profile_detail", pk=pk)
    if request.method == "POST":
        form = MentorshipRequestForm(request.POST)
        if form.is_valid():
            try:
                pairing = request_mentorship(
                    request.user,
                    mentor_profile,
                    topic=form.cleaned_data.get("topic", ""),
                    goals=form.cleaned_data.get("goals", ""),
                    cadence=form.cleaned_data.get("cadence") or None,
                    duration_months=form.cleaned_data.get("duration_months"),
                )
            except ValidationError as exc:
                messages.error(request, str(exc))
                return redirect("alumni_profiles:profile_detail", pk=pk)
            messages.success(request, "Mentorship requested.")
            return redirect("alumni_engagement:mentorship_detail", pk=pairing.pk)
    else:
        form = MentorshipRequestForm()
    return render(
        request,
        "alumni/engagement/mentorship_request_form.html",
        {"form": form, "mentor": mentor_profile},
    )


@login_required
@require_POST
def mentorship_respond_view(request, pk: int) -> HttpResponse:
    pairing = get_object_or_404(MentorshipPairing, pk=pk)
    if pairing.mentor.user_id != request.user.pk and not _user_is_staff(request.user):
        raise Http404("Not authorised.")
    accept = request.POST.get("action") == "accept"
    try:
        respond_to_mentorship(
            pairing,
            accept=accept,
            note=request.POST.get("note", ""),
        )
    except ValidationError as exc:
        messages.error(request, str(exc))
    return redirect("alumni_engagement:mentorship_detail", pk=pairing.pk)


def _user_in_pairing(user, pairing: MentorshipPairing) -> bool:
    if not user.is_authenticated:
        return False
    return user.pk == pairing.mentor.user_id or user.pk == pairing.mentee_id or _user_is_staff(user)


class MentorshipDetailView(LoginRequiredMixin, DetailView):
    model = MentorshipPairing
    template_name = "alumni/engagement/mentorship_detail.html"
    context_object_name = "pairing"

    def get_object(self, queryset=None):
        pairing = super().get_object(queryset=queryset)
        if not _user_in_pairing(self.request.user, pairing):
            raise Http404("Not authorised.")
        return pairing

    def get_context_data(self, **kwargs):
        from apps.alumni.engagement.forms import MentorshipCompleteForm, SessionLogForm

        ctx = super().get_context_data(**kwargs)
        pairing = self.object
        viewer = self.request.user
        ctx["session_form"] = SessionLogForm()
        ctx["complete_form"] = MentorshipCompleteForm()
        ctx["notes"] = list(pairing.session_notes.all().order_by("-created_at")[:50])
        ctx["is_mentor"] = viewer.pk == pairing.mentor.user_id
        ctx["is_mentee"] = viewer.pk == pairing.mentee_id
        return ctx


@login_required
@require_POST
def mentorship_log_session_view(request, pk: int) -> HttpResponse:
    from apps.alumni.engagement.forms import SessionLogForm
    from apps.alumni.engagement.services import log_session

    pairing = get_object_or_404(MentorshipPairing, pk=pk)
    if not _user_in_pairing(request.user, pairing):
        raise Http404("Not authorised.")
    form = SessionLogForm(request.POST)
    if form.is_valid():
        try:
            log_session(pairing, request.user, form.cleaned_data["content"])
            messages.success(request, "Session note saved.")
        except ValidationError as exc:
            messages.error(request, str(exc))
    else:
        messages.error(request, "Session notes cannot be empty.")
    return redirect("alumni_engagement:mentorship_detail", pk=pairing.pk)


@login_required
@require_POST
def mentorship_complete_view(request, pk: int) -> HttpResponse:
    from apps.alumni.engagement.forms import MentorshipCompleteForm
    from apps.alumni.engagement.services import complete_mentorship

    pairing = get_object_or_404(MentorshipPairing, pk=pk)
    if not _user_in_pairing(request.user, pairing):
        raise Http404("Not authorised.")
    form = MentorshipCompleteForm(request.POST)
    if form.is_valid():
        try:
            complete_mentorship(
                pairing,
                outcome=form.cleaned_data["outcome"],
                outcome_rating=form.cleaned_data.get("outcome_rating"),
                completed_by=request.user,
            )
            messages.success(request, "Mentorship marked complete. Thank you for the feedback.")
        except ValidationError as exc:
            messages.error(request, str(exc))
    else:
        messages.error(request, "Add an outcome summary before completing the pairing.")
    return redirect("alumni_engagement:mentorship_detail", pk=pairing.pk)


@login_required
@require_POST
def mentorship_withdraw_view(request, pk: int) -> HttpResponse:
    from apps.alumni.engagement.services import withdraw_mentorship_request

    pairing = get_object_or_404(MentorshipPairing, pk=pk)
    if pairing.mentee_id != request.user.pk:
        raise Http404("Only the mentee can withdraw a request.")
    try:
        withdraw_mentorship_request(pairing, by_user=request.user)
        messages.success(request, "Request withdrawn.")
    except ValidationError as exc:
        messages.error(request, str(exc))
    return redirect("alumni_engagement:mentorship_list")


# ---------------------------------------------------------------------------
# Groups
# ---------------------------------------------------------------------------


class NetworkGroupListView(ListView):
    model = NetworkGroup
    template_name = "alumni/engagement/network_groups.html"
    context_object_name = "groups"
    paginate_by = 40

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        viewer = self.request.user
        joined_ids: set[int] = set()
        moderated_ids: set[int] = set()
        if viewer.is_authenticated:
            for m in GroupMembership.objects.filter(user=viewer).values(
                "group_id", "role"
            ):
                joined_ids.add(m["group_id"])
                if m["role"] == "moderator":
                    moderated_ids.add(m["group_id"])
        ctx["joined_group_ids"] = joined_ids
        ctx["moderated_group_ids"] = moderated_ids
        ctx["joined_count"] = len(joined_ids)
        return ctx


class NetworkGroupCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView):
    model = NetworkGroup
    form_class = NetworkGroupForm
    template_name = "alumni/engagement/network_group_form.html"

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

    def form_valid(self, form):
        from django.contrib import messages

        self.object = form.save()
        # Creator is automatically the first moderator of the group.
        from apps.alumni.engagement.services import join_group

        membership = join_group(self.request.user, self.object)
        membership.role = "moderator"
        membership.save(update_fields=["role"])
        messages.success(self.request, f"Group “{self.object.title}” created.")
        return redirect(
            "alumni_engagement:network_group_detail", slug=self.object.slug
        )


class NetworkGroupDetailView(DetailView):
    model = NetworkGroup
    template_name = "alumni/engagement/network_group_detail.html"
    context_object_name = "group"
    slug_field = "slug"
    slug_url_kwarg = "slug"

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

        from apps.alumni.profiles.models import AlumniProfile
        from apps.alumni.tracking.models import CareerMilestone

        ctx = super().get_context_data(**kwargs)
        viewer = self.request.user
        group = self.object

        # Membership state for the current viewer.
        ctx["is_member"] = (
            viewer.is_authenticated
            and GroupMembership.objects.filter(user=viewer, group=group).exists()
        )
        ctx["is_moderator"] = _user_moderates_group(viewer, group)

        # Chapter-leadership request state: a plain member (not already a
        # moderator/leader) may ask an alumni officer to make them a leader.
        is_plain_member = ctx["is_member"] and not GroupMembership.objects.filter(
            user=viewer, group=group, role="moderator"
        ).exists()
        ctx["has_pending_lead_request"] = viewer.is_authenticated and (
            ChapterLeadershipRequest.objects.filter(
                user=viewer, group=group, status=LeadershipRequestStatus.PENDING
            ).exists()
        )
        ctx["can_request_lead"] = (
            is_plain_member
            and not _user_is_staff(viewer)
            and not ctx["has_pending_lead_request"]
        )

        # Moderators + member preview (both paginated-lite — 12 max in UI).
        memberships = (
            GroupMembership.objects.filter(group=group)
            .select_related("user", "user__profile", "user__alumni_profile")
            .order_by("-joined_at")
        )
        ctx["moderators"] = list(memberships.filter(role="moderator")[:6])
        ctx["member_memberships"] = list(memberships[:12])
        ctx["total_members"] = group.memberships.count()

        # Alumni profiles among the members (for a lively card grid).
        member_user_ids = list(memberships.values_list("user_id", flat=True)[:36])
        ctx["member_profiles"] = list(
            AlumniProfile.objects.filter(
                user_id__in=member_user_ids, visibility_consent=True
            )
            .select_related("user", "current_institution")
            .order_by("-published_at")[:8]
        )

        # Upcoming events scoped to this group.
        ctx["upcoming_events"] = list(
            group.events.filter(end_at__gte=timezone.now()).order_by("start_at")[:6]
        )

        # Announcements posted by moderators — surfaced to all members so they
        # can read the group's discussion feed (previously visible only on the
        # moderator manage dashboard).
        ctx["announcements"] = list(
            group.announcements.select_related("author").order_by("-created_at")[:10]
        )

        # Recent member milestones — quick "what's happening" stream.
        ctx["recent_milestones"] = list(
            CareerMilestone.objects.filter(
                profile__user_id__in=member_user_ids, visibility="public"
            )
            .select_related("profile", "profile__user")
            .order_by("-occurred_on")[:5]
        )

        # Lightweight stats: countries + sectors represented.
        profile_qs = AlumniProfile.objects.filter(user_id__in=member_user_ids)
        country_rows = (
            profile_qs.values("user__profile__country")
            .exclude(user__profile__country__isnull=True)
            .exclude(user__profile__country="")
            .annotate(total=Count("pk"))
            .order_by("-total")[:6]
        )
        ctx["country_breakdown"] = [
            {"code": row["user__profile__country"], "total": row["total"]}
            for row in country_rows
        ]

        # Sibling groups of the same kind (helps discovery from an empty page).
        ctx["related_groups"] = list(
            NetworkGroup.objects.filter(kind=group.kind)
            .exclude(pk=group.pk)
            .order_by("-member_count", "title")[:4]
        )

        return ctx


@login_required
@require_POST
def group_join_view(request, slug: str) -> HttpResponse:
    group = get_object_or_404(NetworkGroup, slug=slug)
    join_group(request.user, group)
    return redirect("alumni_engagement:network_group_detail", slug=group.slug)


@login_required
@require_POST
def group_leave_view(request, slug: str) -> HttpResponse:
    group = get_object_or_404(NetworkGroup, slug=slug)
    leave_group(request.user, group)
    return redirect("alumni_engagement:network_group_detail", slug=group.slug)


# ---------------------------------------------------------------------------
# Group moderator / management dashboard
# ---------------------------------------------------------------------------


def _moderator_guard(view_func):
    """Decorator: require authenticated user who moderates this group (or is staff)."""

    def wrapper(request, slug: str, *args, **kwargs):
        if not request.user.is_authenticated:
            return redirect("login")
        group = get_object_or_404(NetworkGroup, slug=slug)
        if not _user_moderates_group(request.user, group):
            raise Http404("You do not manage this group.")
        return view_func(request, group, *args, **kwargs)

    return wrapper


class GroupManageView(LoginRequiredMixin, UserPassesTestMixin, DetailView):
    """Moderator-only dashboard for managing a group."""

    model = NetworkGroup
    template_name = "alumni/engagement/network_group_manage.html"
    context_object_name = "group"
    slug_field = "slug"
    slug_url_kwarg = "slug"

    def test_func(self):
        return _user_moderates_group(self.request.user, self.get_object())

    def get_context_data(self, **kwargs):
        from django.utils import timezone

        from apps.alumni.engagement.models import GroupAnnouncement

        ctx = super().get_context_data(**kwargs)
        group = self.object
        now = timezone.now()

        ctx["memberships"] = list(
            GroupMembership.objects.filter(group=group)
            .select_related("user", "user__alumni_profile")
            .order_by("-role", "-joined_at")
        )
        ctx["moderator_count"] = sum(
            1 for m in ctx["memberships"] if m.role == "moderator"
        )
        ctx["upcoming_events"] = list(
            group.events.filter(end_at__gte=now).order_by("start_at")[:10]
        )
        ctx["past_events"] = list(
            group.events.filter(end_at__lt=now).order_by("-start_at")[:10]
        )
        ctx["recent_announcements"] = list(
            GroupAnnouncement.objects.filter(group=group).order_by("-created_at")[:5]
        )
        return ctx


class GroupEditView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
    """Edit a group's identity — moderators of this group only."""

    model = NetworkGroup
    form_class = NetworkGroupForm
    template_name = "alumni/engagement/network_group_form.html"
    slug_field = "slug"
    slug_url_kwarg = "slug"

    def test_func(self):
        return _user_moderates_group(self.request.user, self.get_object())

    def form_valid(self, form):
        self.object = form.save()
        messages.success(self.request, f"“{self.object.title}” updated.")
        return redirect(
            "alumni_engagement:network_group_manage", slug=self.object.slug
        )


@login_required
@require_POST
def group_announce_view(request, slug: str) -> HttpResponse:
    group = get_object_or_404(NetworkGroup, slug=slug)
    if not _user_moderates_group(request.user, group):
        raise Http404("You do not manage this group.")
    title = (request.POST.get("title") or "").strip()
    body = (request.POST.get("body") or "").strip()
    if not title or not body:
        messages.error(request, "Announcement title and body are required.")
        return redirect("alumni_engagement:network_group_manage", slug=group.slug)
    post_group_announcement(group, author=request.user, title=title, body=body)
    suffix = "s" if group.member_count != 1 else ""
    messages.success(
        request,
        f"Announcement sent to {group.member_count} member{suffix}.",
    )
    return redirect("alumni_engagement:network_group_manage", slug=group.slug)


@login_required
@require_POST
def group_member_role_view(request, slug: str, user_pk: int) -> HttpResponse:
    from django.contrib.auth import get_user_model

    group = get_object_or_404(NetworkGroup, slug=slug)
    if not _user_moderates_group(request.user, group):
        raise Http404("You do not manage this group.")
    target = get_object_or_404(get_user_model(), pk=user_pk)
    role = (request.POST.get("role") or "").strip() or "member"
    try:
        set_member_role(group, target, role)
        messages.success(request, f"{target} role set to {role}.")
    except ValidationError as exc:
        messages.error(request, str(exc))
    return redirect("alumni_engagement:network_group_manage", slug=group.slug)


@login_required
@require_POST
def group_member_remove_view(request, slug: str, user_pk: int) -> HttpResponse:
    from django.contrib.auth import get_user_model

    group = get_object_or_404(NetworkGroup, slug=slug)
    if not _user_moderates_group(request.user, group):
        raise Http404("You do not manage this group.")
    target = get_object_or_404(get_user_model(), pk=user_pk)
    if target.pk == request.user.pk:
        messages.error(request, "Leave the group from your profile instead.")
    else:
        remove_member(group, target)
        messages.success(request, f"{target} removed from the group.")
    return redirect("alumni_engagement:network_group_manage", slug=group.slug)


# ---------------------------------------------------------------------------
# Chapter leadership requests (member → officer)
# ---------------------------------------------------------------------------


@login_required
@require_POST
def group_lead_request_view(request, slug: str) -> HttpResponse:
    """A member asks an alumni officer to make them a leader of this chapter."""
    group = get_object_or_404(NetworkGroup, slug=slug)
    is_member = GroupMembership.objects.filter(user=request.user, group=group).exists()
    if not is_member:
        messages.error(request, "Join the chapter before requesting to lead it.")
        return redirect("alumni_engagement:network_group_detail", slug=group.slug)
    if _user_moderates_group(request.user, group):
        messages.info(request, "You already lead this chapter.")
        return redirect("alumni_engagement:network_group_detail", slug=group.slug)
    req, created = ChapterLeadershipRequest.objects.get_or_create(
        user=request.user,
        group=group,
        status=LeadershipRequestStatus.PENDING,
        defaults={"message": (request.POST.get("message") or "").strip()},
    )
    if created:
        notify_officers_of_leadership_request(req)
        messages.success(
            request,
            "Your request to lead this chapter has been sent to the alumni officers.",
        )
    else:
        messages.info(request, "You already have a pending request for this chapter.")
    return redirect("alumni_engagement:network_group_detail", slug=group.slug)


class ChapterLeadershipQueueView(LoginRequiredMixin, UserPassesTestMixin, ListView):
    """Alumni officers review members' requests to lead a chapter."""

    template_name = "alumni/engagement/leadership_requests.html"
    context_object_name = "requests"
    paginate_by = 25

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

    def get_queryset(self):
        return (
            ChapterLeadershipRequest.objects.filter(
                status=LeadershipRequestStatus.PENDING
            )
            .select_related("user", "group")
            .order_by("created_at")
        )


@login_required
@require_POST
def leadership_request_approve_view(request, pk: int) -> HttpResponse:
    if not _user_is_staff(request.user):
        raise Http404()
    req = get_object_or_404(ChapterLeadershipRequest, pk=pk)
    if req.status == LeadershipRequestStatus.PENDING:
        # Promote the member to leader (moderator) of the chapter.
        set_member_role(req.group, req.user, "moderator")
        req.status = LeadershipRequestStatus.APPROVED
        req.reviewed_by = request.user
        req.reviewed_at = timezone.now()
        req.save(update_fields=["status", "reviewed_by", "reviewed_at"])
        notify_requester_of_leadership_decision(req, approved=True)
    who = req.user.get_full_name() or req.user.email
    messages.success(request, f"{who} is now a leader of “{req.group.title}”.")
    return redirect("alumni_engagement:leadership_request_queue")


@login_required
@require_POST
def leadership_request_decline_view(request, pk: int) -> HttpResponse:
    if not _user_is_staff(request.user):
        raise Http404()
    req = get_object_or_404(ChapterLeadershipRequest, pk=pk)
    if req.status == LeadershipRequestStatus.PENDING:
        req.status = LeadershipRequestStatus.DECLINED
        req.reviewed_by = request.user
        req.reviewed_at = timezone.now()
        req.review_notes = (request.POST.get("review_notes") or "").strip()
        req.save(update_fields=["status", "reviewed_by", "reviewed_at", "review_notes"])
        notify_requester_of_leadership_decision(req, approved=False)
    messages.success(request, "Leadership request declined and the member notified.")
    return redirect("alumni_engagement:leadership_request_queue")
