from __future__ import annotations

import logging
from datetime import timedelta

from django.core.exceptions import ValidationError
from django.db import transaction
from django.db.models import F
from django.utils import timezone

from apps.alumni.engagement.matcher import (
    MenteeContext,
    explain_match,
    find_candidates,
    score_mentor,
)
from apps.alumni.engagement.models import (
    AlumniEvent,
    EventRegistration,
    EventVisibility,
    GroupMembership,
    MentorshipNote,
    MentorshipPairing,
    MentorshipStatus,
    NetworkGroup,
)
from apps.alumni.profiles.models import AlumniProfile

logger = logging.getLogger(__name__)


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


@transaction.atomic
def create_event(**fields) -> AlumniEvent:
    event = AlumniEvent.objects.create(**fields)
    return event


@transaction.atomic
def register_for_event(user, event: AlumniEvent) -> EventRegistration:
    if event.capacity and event.registrations.count() >= event.capacity:
        raise ValidationError("This event is at capacity.")
    if event.visibility == EventVisibility.GROUP:
        allowed_group_ids = set(event.groups.values_list("pk", flat=True))
        user_group_ids = set(
            GroupMembership.objects.filter(user=user, group_id__in=allowed_group_ids).values_list(
                "group_id", flat=True
            )
        )
        if not user_group_ids:
            raise ValidationError("This event is restricted to specific alumni groups.")
    reg, created = EventRegistration.objects.get_or_create(user=user, event=event)
    if created:
        _notify_event_registered(user, event)
    return reg


@transaction.atomic
def record_event_feedback(
    user, event: AlumniEvent, *, rating: int, text: str = ""
) -> EventRegistration:
    """Store a registered attendee's post-event rating + comment.

    Enforces: the event has ended, the user RSVP'd, and the rating is 1–5.
    Submitting marks the registration as attended. Idempotent — updates the
    existing registration row rather than creating a duplicate.
    """
    if event.end_at >= timezone.now():
        raise ValidationError("Feedback opens once the event has ended.")
    try:
        rating = int(rating)
    except (TypeError, ValueError):
        raise ValidationError("Please choose a rating from 1 to 5.")
    if not (1 <= rating <= 5):
        raise ValidationError("Please choose a rating from 1 to 5.")
    reg = EventRegistration.objects.filter(user=user, event=event).first()
    if reg is None:
        raise ValidationError("Only registered attendees can leave feedback.")
    reg.feedback_rating = rating
    reg.feedback_text = text or ""
    reg.attended = True
    reg.save(update_fields=["feedback_rating", "feedback_text", "attended"])
    return reg


def _notify_event_registered(user, event: AlumniEvent) -> None:
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        send_notification(
            user,
            f"You are registered for {event.title} on {event.start_at:%b %d, %Y}.",
            verb=Notification.Verb.ALUMNI_EVENT_REGISTERED,
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.event notify failed event=%s user=%s", event.pk, user.pk)


def send_due_event_reminders() -> int:
    """24-hour reminder task; runs every 15 min via celery beat."""
    now = timezone.now()
    cutoff = now + timedelta(hours=24)
    sent = 0
    qs = EventRegistration.objects.filter(
        event__start_at__lte=cutoff,
        event__start_at__gt=now,
        reminder_sent_at__isnull=True,
    ).select_related("event", "user")
    for reg in qs:
        try:
            from apps.core.notifications.models import Notification
            from apps.core.notifications.services import send_notification

            send_notification(
                reg.user,
                f"Reminder: {reg.event.title} starts at {reg.event.start_at:%b %d, %Y %H:%M UTC}.",
                verb=Notification.Verb.ALUMNI_EVENT_REMINDER,
            )
            reg.reminder_sent_at = timezone.now()
            reg.save(update_fields=["reminder_sent_at"])
            sent += 1
        except Exception:  # pragma: no cover - defensive
            logger.exception("alumni.event reminder failed registration=%s", reg.pk)
    return sent


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


@transaction.atomic
def join_group(user, group: NetworkGroup) -> GroupMembership:
    membership, created = GroupMembership.objects.get_or_create(user=user, group=group)
    if created:
        NetworkGroup.objects.filter(pk=group.pk).update(member_count=F("member_count") + 1)
    return membership


@transaction.atomic
def leave_group(user, group: NetworkGroup) -> None:
    deleted, _ = GroupMembership.objects.filter(user=user, group=group).delete()
    if deleted:
        NetworkGroup.objects.filter(pk=group.pk).update(
            member_count=F("member_count") - 1
        )


# ---------------------------------------------------------------------------
# Group moderation — promote / demote / remove
# ---------------------------------------------------------------------------


@transaction.atomic
def set_member_role(group: NetworkGroup, user, role: str) -> GroupMembership:
    """Set a member's role (MEMBER or MODERATOR). Creates the membership if missing."""
    from apps.alumni.engagement.models import GroupRole

    if role not in {GroupRole.MEMBER, GroupRole.MODERATOR}:
        raise ValidationError("Invalid group role.")
    membership, _ = GroupMembership.objects.get_or_create(user=user, group=group)
    if membership.role == role:
        return membership
    was_member = membership.role == GroupRole.MEMBER
    membership.role = role
    membership.save(update_fields=["role"])
    if role == GroupRole.MODERATOR and was_member:
        _notify_promoted(user, group)
    return membership


def _notify_promoted(user, group: NetworkGroup) -> None:
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        send_notification(
            user,
            f"You've been promoted to moderator of the {group.title} group.",
            verb=Notification.Verb.ALUMNI_GROUP_MEMBER_PROMOTED,
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.group promote notify failed group=%s", group.pk)


@transaction.atomic
def remove_member(group: NetworkGroup, user) -> None:
    """Remove a member from a group (moderator action)."""
    leave_group(user, group)


# ---------------------------------------------------------------------------
# Group announcements + event reminders
# ---------------------------------------------------------------------------


@transaction.atomic
def post_group_announcement(group: NetworkGroup, *, author, title: str, body: str):
    """Create a GroupAnnouncement and notify every member of the group."""
    from apps.alumni.engagement.models import GroupAnnouncement

    announcement = GroupAnnouncement.objects.create(
        group=group, author=author, title=title, body=body,
    )
    _notify_group_members(
        group,
        verb_key="ALUMNI_GROUP_ANNOUNCEMENT",
        message=f"{group.title}: {title}",
        exclude_user=author,
    )
    return announcement


def announce_event_to_groups(event) -> int:
    """When a group-scoped event is created, notify all members of the linked groups.

    Idempotent — once ``event.announced_at`` is set the function no-ops.
    Returns the number of notifications sent.
    """
    from apps.alumni.engagement.models import EventVisibility

    if event.announced_at is not None:
        return 0
    if event.visibility != EventVisibility.GROUP:
        # Not a group-scoped event; nothing to announce here.
        return 0

    notified: set[int] = set()
    for group in event.groups.all():
        for membership in GroupMembership.objects.filter(group=group).select_related("user"):
            u = membership.user
            if u.pk in notified or (event.organiser_id and u.pk == event.organiser_id):
                continue
            notified.add(u.pk)
            _send_group_event_notification(
                recipient=u,
                group=group,
                event=event,
                verb_key="ALUMNI_GROUP_NEW_EVENT",
                message=f"New event in {group.title}: {event.title} on {event.start_at:%b %d}.",
            )
    event.announced_at = timezone.now()
    event.save(update_fields=["announced_at"])
    return len(notified)


def notify_officers_of_event_submission(event) -> int:
    """Alert alumni officers/admins that an alumnus proposed an event for review."""
    try:
        from django.contrib.auth import get_user_model

        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import bulk_notify

        User = get_user_model()
        officers = User.objects.filter(
            role__in=["alumni_officer", "admin", "system_admin"]
        )
        organiser = event.organiser.get_full_name() if event.organiser else "An alumnus"
        return bulk_notify(
            officers,
            f"New event awaiting approval: “{event.title}” proposed by {organiser}.",
            verb=Notification.Verb.ALUMNI_EVENT_SUBMITTED,
            data={"event_slug": event.slug},
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.event submit notify failed event=%s", event.pk)
        return 0


def notify_organiser_of_event_decision(event, *, approved: bool) -> None:
    """Notify the event organiser that their event was approved or rejected."""
    if not event.organiser_id:
        return
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        if approved:
            verb = Notification.Verb.ALUMNI_EVENT_APPROVED
            message = f"Your event “{event.title}” was approved and is now on the calendar."
        else:
            verb = Notification.Verb.ALUMNI_EVENT_REJECTED
            message = f"Your event “{event.title}” was not approved."
            if event.review_notes:
                message += f" Officer note: {event.review_notes}"
        send_notification(event.organiser, message, verb=verb, data={"event_slug": event.slug})
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.event decision notify failed event=%s", event.pk)


def notify_officers_of_leadership_request(req) -> int:
    """Alert alumni officers/admins that a member asked to lead a chapter."""
    try:
        from django.contrib.auth import get_user_model

        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import bulk_notify

        User = get_user_model()
        officers = User.objects.filter(
            role__in=["alumni_officer", "admin", "system_admin"]
        )
        who = req.user.get_full_name() or req.user.email
        return bulk_notify(
            officers,
            f"{who} has asked to lead the “{req.group.title}” chapter.",
            verb=Notification.Verb.ALUMNI_CHAPTER_LEAD_REQUESTED,
            data={"group_slug": req.group.slug, "request_id": req.pk},
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.chapter lead request notify failed req=%s", req.pk)
        return 0


def notify_requester_of_leadership_decision(req, *, approved: bool) -> None:
    """Notify the member whether their chapter-leadership request was approved."""
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        if approved:
            verb = Notification.Verb.ALUMNI_CHAPTER_LEAD_APPROVED
            message = (
                f"You are now a leader of the “{req.group.title}” chapter. "
                "You can post announcements and manage the group."
            )
        else:
            verb = Notification.Verb.ALUMNI_CHAPTER_LEAD_DECLINED
            message = f"Your request to lead “{req.group.title}” was not approved this time."
            if req.review_notes:
                message += f" Note: {req.review_notes}"
        send_notification(req.user, message, verb=verb, data={"group_slug": req.group.slug})
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.chapter lead decision notify failed req=%s", req.pk)


def _notify_group_members(
    group: NetworkGroup,
    *,
    verb_key: str,
    message: str,
    exclude_user=None,
) -> int:
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification
    except Exception:  # pragma: no cover - defensive
        return 0
    verb = getattr(Notification.Verb, verb_key, Notification.Verb.GENERIC)
    sent = 0
    memberships = GroupMembership.objects.filter(group=group).select_related("user")
    for m in memberships:
        if exclude_user and m.user_id == exclude_user.pk:
            continue
        try:
            send_notification(m.user, message, verb=verb)
            sent += 1
        except Exception:  # pragma: no cover - defensive
            logger.exception("alumni.group notify failed group=%s user=%s", group.pk, m.user_id)
    return sent


def _send_group_event_notification(
    *, recipient, group, event, verb_key: str, message: str
) -> None:
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        verb = getattr(Notification.Verb, verb_key, Notification.Verb.GENERIC)
        send_notification(recipient, message, verb=verb)
    except Exception:  # pragma: no cover - defensive
        logger.exception(
            "alumni.group event notify failed group=%s event=%s user=%s",
            group.pk, event.pk, recipient.pk,
        )


def send_group_event_nudges(*, hours_ahead: int = 48) -> int:
    """Nudge group members about upcoming group events they haven't RSVP'd for.

    Fires once per event (``group_reminder_sent_at`` gate). Runs via celery beat.
    """
    from apps.alumni.engagement.models import EventVisibility

    now = timezone.now()
    cutoff = now + timedelta(hours=hours_ahead)
    events = AlumniEvent.objects.filter(
        visibility=EventVisibility.GROUP,
        start_at__lte=cutoff,
        start_at__gte=now,
        group_reminder_sent_at__isnull=True,
    ).prefetch_related("groups", "registrations")

    total = 0
    for event in events:
        registered_ids = set(
            event.registrations.values_list("user_id", flat=True)
        )
        notified: set[int] = set()
        for group in event.groups.all():
            memberships = GroupMembership.objects.filter(group=group).select_related("user")
            for m in memberships:
                if m.user_id in notified or m.user_id in registered_ids:
                    continue
                notified.add(m.user_id)
                _send_group_event_notification(
                    recipient=m.user,
                    group=group,
                    event=event,
                    verb_key="ALUMNI_GROUP_EVENT_NUDGE",
                    message=(
                        f"{group.title} — {event.title} starts "
                        f"{event.start_at:%b %d, %H:%M UTC}. RSVP if you're coming."
                    ),
                )
        event.group_reminder_sent_at = now
        event.save(update_fields=["group_reminder_sent_at"])
        total += len(notified)
    return total


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


def _build_mentee_context(mentee, *, topic: str = "") -> MenteeContext:
    """Assemble a :class:`MenteeContext` from a mentee user for scoring.

    Mirrors the context construction in :func:`recommend_mentors` so the score
    stored at request time matches what the discover page showed.
    """
    profile = getattr(mentee, "alumni_profile", None)
    tags = list(profile.expertise_tags) if profile and profile.expertise_tags else []
    country = getattr(getattr(mentee, "profile", None), "country", "") or ""
    sector = ""
    if profile is not None:
        latest = profile.employments.order_by("-started_on").first()
        if latest is not None:
            sector = latest.sector or ""
    return MenteeContext(
        user_id=mentee.pk,
        expertise_tags=tags,
        country=country,
        sector=sector,
        topic=topic or None,
    )


def _compute_match_score(mentor_profile: AlumniProfile, mentee, *, topic: str, active: int) -> float:
    """Best-effort match score in 0–1. Never raises — a scoring failure or a
    missing mentee profile degrades gracefully to 0.0 so it can't block a
    mentorship request."""
    try:
        context = _build_mentee_context(mentee, topic=topic)
        return round(
            score_mentor(mentor_profile, context, mentor_active_pairings=active), 4
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception(
            "alumni.mentorship score failed mentor=%s mentee=%s",
            mentor_profile.pk,
            getattr(mentee, "pk", None),
        )
        return 0.0


@transaction.atomic
def request_mentorship(
    mentee,
    mentor_profile: AlumniProfile,
    *,
    topic: str = "",
    goals: str = "",
    cadence: str | None = None,
    duration_months: int | None = None,
) -> MentorshipPairing:
    from apps.alumni.engagement.models import MentorshipCadence

    if mentor_profile.user_id == mentee.pk:
        raise ValidationError("Cannot mentor yourself.")
    if not mentor_profile.mentor_accepting:
        raise ValidationError("This alumnus is not currently accepting new mentees.")

    active = MentorshipPairing.objects.filter(
        mentor=mentor_profile, status=MentorshipStatus.ACTIVE
    ).count()
    if mentor_profile.mentor_capacity and active >= mentor_profile.mentor_capacity:
        raise ValidationError(
            "This mentor has reached their capacity. Try another mentor or check back later."
        )

    match_score = _compute_match_score(
        mentor_profile, mentee, topic=topic, active=active
    )
    cadence_value = cadence or MentorshipCadence.MONTHLY
    pairing, created = MentorshipPairing.objects.get_or_create(
        mentor=mentor_profile,
        mentee=mentee,
        topic=topic,
        defaults={
            "status": MentorshipStatus.REQUESTED,
            "goals": goals,
            "cadence": cadence_value,
            "duration_months": duration_months,
            "match_score": match_score,
        },
    )
    if not created:
        # Refresh kickoff fields if re-requested (rare) — allows edit pre-accept.
        dirty = []
        if goals and pairing.goals != goals:
            pairing.goals = goals
            dirty.append("goals")
        if cadence and pairing.cadence != cadence:
            pairing.cadence = cadence
            dirty.append("cadence")
        if duration_months and pairing.duration_months != duration_months:
            pairing.duration_months = duration_months
            dirty.append("duration_months")
        # Re-score a still-requested pairing so an edited topic/context is reflected.
        if pairing.status == MentorshipStatus.REQUESTED and pairing.match_score != match_score:
            pairing.match_score = match_score
            dirty.append("match_score")
        if dirty:
            pairing.save(update_fields=dirty)
    if created:
        _notify_mentorship_requested(mentor_profile.user, mentee, pairing)
    return pairing


def _notify_mentorship_requested(mentor_user, mentee, pairing: MentorshipPairing) -> None:
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        send_notification(
            mentor_user,
            f"{mentee} requested mentorship on '{pairing.topic}'.",
            verb=Notification.Verb.ALUMNI_MENTORSHIP_REQUESTED,
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.mentorship notify failed pairing=%s", pairing.pk)


@transaction.atomic
def respond_to_mentorship(
    pairing: MentorshipPairing,
    *,
    accept: bool,
    note: str = "",
) -> MentorshipPairing:
    if pairing.status != MentorshipStatus.REQUESTED:
        raise ValidationError("Pairing is no longer in a requested state.")
    if accept:
        pairing.status = MentorshipStatus.ACTIVE
        pairing.started_at = timezone.now()
    else:
        pairing.status = MentorshipStatus.DECLINED
    pairing.save(update_fields=["status", "started_at"])
    if note:
        MentorshipNote.objects.create(
            pairing=pairing, author=pairing.mentor.user, content=note
        )
    _notify_mentorship_response(pairing, accept)
    return pairing


def _notify_mentorship_response(pairing: MentorshipPairing, accept: bool) -> None:
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        verb = (
            Notification.Verb.ALUMNI_MENTORSHIP_ACCEPTED
            if accept
            else Notification.Verb.ALUMNI_MENTORSHIP_DECLINED
        )
        message = (
            f"{pairing.mentor.display_name} accepted your mentorship request."
            if accept
            else f"{pairing.mentor.display_name} declined your mentorship request."
        )
        send_notification(pairing.mentee, message, verb=verb)
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.mentorship response notify failed pairing=%s", pairing.pk)


@transaction.atomic
def log_session(
    pairing: MentorshipPairing, author, content: str
) -> MentorshipNote:
    if pairing.status != MentorshipStatus.ACTIVE:
        raise ValidationError("Sessions can only be logged on active pairings.")
    note = MentorshipNote.objects.create(pairing=pairing, author=author, content=content)
    # Notify the other party so they see the session in their inbox.
    other = (
        pairing.mentee
        if author.pk == pairing.mentor.user_id
        else pairing.mentor.user
    )
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        send_notification(
            other,
            f"New session note on your mentorship with {pairing.mentor.display_name}.",
            verb=Notification.Verb.ALUMNI_MENTORSHIP_SESSION_LOGGED,
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.mentorship session notify failed pairing=%s", pairing.pk)
    return note


@transaction.atomic
def complete_mentorship(
    pairing: MentorshipPairing,
    *,
    outcome: str = "",
    outcome_rating: int | None = None,
    completed_by=None,
) -> MentorshipPairing:
    """Close an active pairing with an outcome summary + optional rating."""
    if pairing.status != MentorshipStatus.ACTIVE:
        raise ValidationError("Only active pairings can be completed.")
    pairing.status = MentorshipStatus.COMPLETED
    pairing.ended_at = timezone.now()
    if outcome:
        pairing.outcome = outcome
    if outcome_rating is not None:
        if not (1 <= outcome_rating <= 5):
            raise ValidationError("Outcome rating must be between 1 and 5.")
        pairing.outcome_rating = outcome_rating
    pairing.save(update_fields=["status", "ended_at", "outcome", "outcome_rating"])
    # Notify the counterparty of the completion.
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        notify_user = (
            pairing.mentee
            if completed_by and completed_by.pk == pairing.mentor.user_id
            else pairing.mentor.user
        )
        send_notification(
            notify_user,
            f"Mentorship on '{pairing.topic or pairing.goals[:60]}' was marked complete.",
            verb=Notification.Verb.ALUMNI_MENTORSHIP_COMPLETED,
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.mentorship complete notify failed pairing=%s", pairing.pk)
    return pairing


@transaction.atomic
def withdraw_mentorship_request(
    pairing: MentorshipPairing, *, by_user
) -> MentorshipPairing:
    """Mentee cancels a REQUESTED pairing before the mentor responds."""
    if pairing.mentee_id != by_user.pk:
        raise ValidationError("Only the mentee can withdraw a request.")
    if pairing.status != MentorshipStatus.REQUESTED:
        raise ValidationError("Only requested pairings can be withdrawn.")
    pairing.status = MentorshipStatus.CANCELLED
    pairing.ended_at = timezone.now()
    pairing.save(update_fields=["status", "ended_at"])
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        send_notification(
            pairing.mentor.user,
            f"{by_user} withdrew their mentorship request.",
            verb=Notification.Verb.ALUMNI_MENTORSHIP_WITHDRAWN,
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.mentorship withdraw notify failed pairing=%s", pairing.pk)
    return pairing


def recommend_mentors(
    mentee,
    *,
    limit: int = 10,
    topic: str | None = None,
    country_override: str | None = None,
    extra_tags: list[str] | None = None,
    include_unavailable: bool = False,
):
    """Return (profile, score, explainer) triples for the discover page.

    ``topic`` / ``country_override`` / ``extra_tags`` let the discover page
    refine the matcher context per search without persisting changes to the
    mentee's profile. ``explainer`` is a :class:`MatchExplainer` powering the
    per-card "Shared: …" transparency line.
    """
    profile = getattr(mentee, "alumni_profile", None)
    tags = list(profile.expertise_tags) if profile else []
    if extra_tags:
        tags = list(dict.fromkeys(tags + [t.strip().lower() for t in extra_tags if t.strip()]))
    country = country_override or (
        getattr(getattr(mentee, "profile", None), "country", "") or ""
    )
    sector = ""
    if profile is not None:
        latest = profile.employments.order_by("-started_on").first()
        if latest is not None:
            sector = latest.sector or ""
    context = MenteeContext(
        user_id=mentee.pk,
        expertise_tags=tags,
        country=country,
        sector=sector,
        topic=topic,
    )
    candidates = find_candidates(
        context, limit=limit, include_unavailable=include_unavailable
    )
    return [(p, score, explain_match(p, context)) for p, score in candidates]
