"""Celery tasks for the Alumni module.

Lives at the package root because these tasks straddle all three subapps
(profiles, tracking, engagement) — mirrors ``apps/mel/tracking/tasks.py``.
"""
from __future__ import annotations

import logging

from celery import shared_task
from django.db import transaction  # noqa: F401  (used by other tasks below)
from django.utils import timezone

logger = logging.getLogger(__name__)

# NOTE: ``drain_alumni_outbox`` was removed with the REP LMS — it drained REP's
# IntegrationOutbox (ALUMNI target) into profiles. The payload-based
# ``apps.alumni.profiles.services.create_from_rep_completion`` remains and can be
# fed by a Moodle-completion source in future.


@shared_task
def sync_orcid_for_all_profiles() -> dict:
    """Weekly ORCID sync across every profile with an orcid_id set."""
    from apps.alumni.profiles.models import AlumniProfile
    from apps.alumni.tracking.services import sync_orcid

    synced = 0
    failed = 0
    for profile in AlumniProfile.objects.exclude(orcid_id=""):
        try:
            sync_orcid(profile)
            synced += 1
        except Exception:  # pragma: no cover - individual failures logged in service
            logger.exception("alumni.orcid sync failed profile=%s", profile.pk)
            failed += 1
    logger.info("alumni.orcid sync synced=%s failed=%s", synced, failed)
    return {"synced": synced, "failed": failed}


@shared_task
def snapshot_impact_metrics(period_label: str | None = None) -> dict:
    """Monthly aggregation snapshot for donor dashboards."""
    from apps.alumni.tracking.services import (
        build_impact_snapshot,
        current_period_label,
    )

    label = period_label or current_period_label()
    snapshot = build_impact_snapshot(label)
    return {
        "period_label": snapshot.period_label,
        "total_alumni": snapshot.total_alumni,
    }


@shared_task
def send_event_reminders() -> dict:
    """Fire 24h reminders for upcoming alumni events."""
    from apps.alumni.engagement.services import send_due_event_reminders

    count = send_due_event_reminders()
    return {"sent": count}


@shared_task
def send_group_event_nudges() -> dict:
    """Nudge members of a group about upcoming group-scoped events (48h window)."""
    from apps.alumni.engagement.services import send_group_event_nudges as _nudge

    count = _nudge()
    return {"nudged": count}


@shared_task
def send_spotlight_digest() -> dict:
    """Weekly digest task — notify consenting alumni of currently-active spotlights."""
    from apps.alumni.profiles.models import AlumniProfile
    from apps.alumni.recognition.services import current_spotlights
    from apps.core.notifications.models import Notification
    from apps.core.notifications.services import send_notification

    spotlights = list(current_spotlights())
    if not spotlights:
        return {"sent": 0, "spotlights": 0}

    headline = ", ".join(
        f"{s.get_category_display()}: {s.profile.display_name}" for s in spotlights[:3]
    )
    message = f"This week's alumni network spotlights — {headline}."

    sent = 0
    for profile in AlumniProfile.objects.filter(visibility_consent=True).select_related("user"):
        try:
            send_notification(
                profile.user,
                message,
                verb=Notification.Verb.ALUMNI_SPOTLIGHT_DIGEST,
            )
            sent += 1
        except Exception:  # pragma: no cover - defensive
            logger.exception("alumni.spotlight digest failed user=%s", profile.user_id)
    logger.info("alumni.spotlight digest sent=%s spotlights=%s", sent, len(spotlights))
    return {"sent": sent, "spotlights": len(spotlights)}


@shared_task
def send_due_scheduled_broadcasts() -> dict:
    """Send officer broadcasts whose ``scheduled_for`` has arrived.

    Fired by celery beat (~every 5 min). Reuses the SAME dispatch path as the
    manual "Send now" button (:func:`dispatch_broadcast`) so scheduled and
    manual sends stay identical — no duplicated routing logic. Each send is
    isolated in try/except so one failure doesn't block the rest of the batch;
    ``dispatch_broadcast`` itself marks the row SENT (with ``sent_at``) on
    success or FAILED on error.
    """
    from apps.alumni.recognition.models import Broadcast, BroadcastStatus
    from apps.alumni.recognition.services import dispatch_broadcast

    now = timezone.now()
    due = Broadcast.objects.filter(
        status=BroadcastStatus.SCHEDULED,
        scheduled_for__lte=now,
    ).order_by("scheduled_for")

    sent = 0
    failed = 0
    for broadcast in due:
        try:
            dispatch_broadcast(broadcast)
            sent += 1
        except Exception:  # pragma: no cover - dispatch marks the row FAILED
            logger.exception(
                "alumni.broadcast scheduled send failed broadcast=%s", broadcast.pk
            )
            failed += 1
    logger.info("alumni.broadcast scheduled sent=%s failed=%s", sent, failed)
    return {"sent": sent, "failed": failed}


@shared_task(
    bind=True,
    autoretry_for=(Exception,),
    retry_backoff=True,
    retry_kwargs={"max_retries": 3},
    retry_backoff_max=600,
)
def embed_profile_async(self, profile_id: int) -> dict:
    """Compute the vector embedding for a single AlumniProfile.

    Idempotent on the source-text hash — called from post_save signals on
    AlumniProfile / Employment / Publication / Degree.
    """
    from apps.alumni.profiles.embeddings import compute_profile_embedding
    from apps.alumni.profiles.models import AlumniProfile

    try:
        profile = AlumniProfile.objects.get(pk=profile_id)
    except AlumniProfile.DoesNotExist:
        return {"profile_id": profile_id, "skipped": "missing"}
    obj = compute_profile_embedding(profile)
    return {"profile_id": profile_id, "embedded": bool(obj)}


@shared_task
def backfill_alumni_embeddings(force: bool = False) -> dict:
    """Backfill embeddings across the whole directory.

    Set ``force=True`` to recompute even when the source-text hash matches.
    Runs weekly via celery beat; safe to invoke ad-hoc from the shell.
    """
    from apps.alumni.profiles.embeddings import compute_profile_embedding
    from apps.alumni.profiles.models import AlumniProfile

    written = 0
    skipped = 0
    for profile in AlumniProfile.objects.iterator():
        before = getattr(profile, "embedding_row", None)
        result = compute_profile_embedding(profile, force=force)
        if result is None:
            skipped += 1
            continue
        if before is None or force or before.source_text_hash != result.source_text_hash:
            written += 1
        else:
            skipped += 1
    logger.info("alumni.embedding backfill written=%s skipped=%s", written, skipped)
    return {"written": written, "skipped": skipped}
