"""Recognition services.

Two service surfaces:
- ``endorse`` — community-driven nomination, idempotent per (profile, endorser, category, period).
- ``publish_spotlight`` — staff/officer-driven curation, deactivates any prior spotlight in the same
  (category, period_label) so only one is active at a time.
"""
from __future__ import annotations

import logging
from datetime import date
from typing import Any

from django.db import transaction
from django.db.models import Count

from apps.alumni.profiles.models import AlumniProfile
from apps.alumni.recognition.models import (
    Broadcast,
    BroadcastAudience,
    BroadcastStatus,
    Endorsement,
    Spotlight,
    SpotlightCategory,
)

logger = logging.getLogger(__name__)


@transaction.atomic
def endorse(
    profile: AlumniProfile,
    endorser,
    *,
    category: str,
    period_label: str,
    citation: str = "",
) -> Endorsement:
    """Record a community endorsement. Idempotent — second call returns the existing row."""
    obj, created = Endorsement.objects.get_or_create(
        profile=profile,
        endorser=endorser,
        category=category,
        period_label=period_label,
        defaults={"citation": citation},
    )
    if created:
        _notify_endorsement(profile, endorser, category, citation)
    return obj


def _notify_endorsement(profile, endorser, category, citation) -> None:
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        category_label = dict(SpotlightCategory.choices).get(category, category)
        send_notification(
            profile.user,
            f"You've been endorsed for {category_label}.",
            verb=Notification.Verb.ALUMNI_SPOTLIGHT_ENDORSED,
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.recognition endorse notify failed profile=%s", profile.pk)


@transaction.atomic
def publish_spotlight(
    profile: AlumniProfile,
    *,
    category: str,
    period_label: str,
    title: str,
    citation: str,
    curator,
    featured_from: date,
    featured_until: date,
    cover_image=None,
    nominated_by=None,
) -> Spotlight:
    """Publish a curated spotlight — atomically deactivates any prior winner in the same period."""
    Spotlight.objects.filter(
        category=category, period_label=period_label, is_active=True
    ).update(is_active=False)
    spotlight, _ = Spotlight.objects.update_or_create(
        category=category,
        period_label=period_label,
        defaults={
            "profile": profile,
            "title": title,
            "citation": citation,
            "cover_image": cover_image,
            "featured_from": featured_from,
            "featured_until": featured_until,
            "is_active": True,
            "curated_by": curator,
            "nominated_by": nominated_by,
        },
    )
    _notify_featured(profile, spotlight)
    _audit_publish(curator, spotlight)
    _record_to_mel(spotlight)
    return spotlight


def _notify_featured(profile, spotlight: Spotlight) -> None:
    try:
        from apps.core.notifications.models import Notification
        from apps.core.notifications.services import send_notification

        send_notification(
            profile.user,
            f"You've been selected as {spotlight.get_category_display()} — {spotlight.title}.",
            verb=Notification.Verb.ALUMNI_SPOTLIGHT_FEATURED,
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.recognition feature notify failed spotlight=%s", spotlight.pk)


def _audit_publish(curator, spotlight: Spotlight) -> None:
    try:
        from apps.core.audit.mixins import log_audit
        from apps.core.audit.models import AuditLog

        log_audit(
            actor=curator,
            action=AuditLog.Action.CREATE,
            target_app="alumni_recognition",
            target_model="Spotlight",
            object_id=str(spotlight.pk),
            object_repr=str(spotlight),
            changes={
                "category": spotlight.category,
                "period_label": spotlight.period_label,
                "profile_id": spotlight.profile_id,
            },
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("alumni.recognition audit failed spotlight=%s", spotlight.pk)


def _record_to_mel(spotlight: Spotlight) -> None:
    try:
        from apps.mel.indicators.services import record_automated_point

        record_automated_point(
            indicator_code="alumni-spotlights-published",
            source_module="alumni",
            source_event="spotlight_published",
            source_object_id=str(spotlight.pk),
            value=1,
        )
    except Exception:  # pragma: no cover - defensive
        logger.exception("mel.indicator record failed for spotlight=%s", spotlight.pk)


def current_spotlights():
    today = date.today()
    return (
        Spotlight.objects.filter(
            is_active=True,
            featured_from__lte=today,
            featured_until__gte=today,
        )
        .select_related("profile", "profile__user")
        .order_by("-featured_from")
    )


def preview_recipients(audience: str, audience_filter: dict | None = None):
    """Return a queryset of `User` rows that match the broadcast audience filters.

    Mirrors the routing in :func:`dispatch_broadcast` so the compose UI can show
    an exact recipient count via HTMX before sending.
    """
    from django.contrib.auth import get_user_model

    User = get_user_model()
    af = audience_filter or {}
    qs = AlumniProfile.objects.filter(visibility_consent=True).select_related("user")
    if audience == BroadcastAudience.MENTORS:
        qs = qs.filter(mentor_accepting=True)
    elif audience == BroadcastAudience.BY_COUNTRY:
        country = (af.get("country") or "").strip()
        if country:
            qs = qs.filter(user__profile__country__iexact=country)
    elif audience == BroadcastAudience.BY_GRADUATION_YEAR:
        year = af.get("graduation_year")
        if year:
            qs = qs.filter(degrees__graduated_on__year=int(year)).distinct()
    elif audience == BroadcastAudience.BY_PROGRAMME:
        programme = (af.get("programme_name") or "").strip()
        if programme:
            qs = qs.filter(degrees__programme_name__icontains=programme).distinct()
    elif audience == BroadcastAudience.BY_GROUP:
        from apps.alumni.engagement.models import GroupMembership

        ids = af.get("group_ids") or []
        if ids:
            user_ids = GroupMembership.objects.filter(group_id__in=ids).values_list("user_id", flat=True)
            return User.objects.filter(pk__in=user_ids, alumni_profile__visibility_consent=True).distinct()
        return User.objects.none()
    return User.objects.filter(pk__in=qs.values_list("user_id", flat=True))


@transaction.atomic
def dispatch_broadcast(broadcast: Broadcast) -> Broadcast:
    """Send a broadcast to its filtered audience via :func:`bulk_notify`.

    Idempotent on ``status='sent'`` — calling twice is a no-op. Failures land on
    ``last_error`` and ``status='failed'`` so the UI can surface the issue.
    """
    if broadcast.status == BroadcastStatus.SENT:
        return broadcast
    from django.utils import timezone

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

    broadcast.status = BroadcastStatus.SENDING
    broadcast.save(update_fields=["status", "updated_at"])

    try:
        recipients = preview_recipients(broadcast.audience, broadcast.audience_filter)
        recipient_count = recipients.count()
        bulk_notify(
            recipients,
            broadcast.subject,
            verb=Notification.Verb.ALUMNI_BROADCAST,
            email_context={
                "subject": broadcast.subject,
                "body_html": broadcast.body_html,
                "broadcast_id": broadcast.pk,
            },
        )
    except Exception as exc:  # pragma: no cover - defensive
        broadcast.status = BroadcastStatus.FAILED
        broadcast.last_error = str(exc)[:2000]
        broadcast.save(update_fields=["status", "last_error", "updated_at"])
        logger.exception("alumni.broadcast dispatch failed broadcast=%s", broadcast.pk)
        raise

    broadcast.status = BroadcastStatus.SENT
    broadcast.sent_at = timezone.now()
    broadcast.sent_count = recipient_count
    broadcast.last_error = ""
    broadcast.save(update_fields=["status", "sent_at", "sent_count", "last_error", "updated_at"])
    return broadcast


def top_endorsement_candidates(
    *, category: str, period_label: str, limit: int = 10
) -> list[dict[str, Any]]:
    rows = (
        Endorsement.objects.filter(category=category, period_label=period_label)
        .values("profile_id")
        .annotate(n=Count("pk"))
        .order_by("-n", "profile_id")[:limit]
    )
    rows = list(rows)
    if not rows:
        return []
    profiles = {
        p.pk: p
        for p in AlumniProfile.objects.filter(
            pk__in=[row["profile_id"] for row in rows]
        ).select_related("user")
    }
    return [
        {"profile": profiles.get(row["profile_id"]), "count": row["n"]}
        for row in rows
        if profiles.get(row["profile_id"])
    ]
